并发包下面,实现了一堆常用的线程安全的数据结构,多个线程访问HashMap是线程安全的,不会把HashMap里的数据改错,ConcurrentHashMap。ArrayList数据结构,内存里面,多线程要并发的访问这个数据结构
CopyOnWriteArrayList,写时复制机制的ArrayList,可以保证线程并发的安全性
csharp
List<String> list = new CopyOnWriteArrayList<String>();
list.add("张三");
list.set(0,"李四");
list.remove(0);
构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
CopyOnWriteArrayList其实也是底层基于数组来实现的,List数据结构,要实现各种线程安全性
css
final void setArray(Object[] a) {
array = a;
}
java
private transient volatile Object[] array;
核心的底层数据结构是数组,volatile,保证 多线程读写的可见性,只要有一个线程修改了这个数组,其他的线程立马是可以读到的
ini
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite:写时复制的机制,大量的写操作都是基于写时复制的机制来实现的
elements:代表的是当前CopyOnWriteArrayList内部的数组,Arrays.copyOf复制的操作,就是把当前数组复制到了新的数组里去,新的数组的长度是多少呢?len + 1,newElements,就是一个全新的数组
新数组里包含了老数组所有的元素,而且长度还多了1位
CopyOnWrite,写数据的时候,不是直接在当前的数组里写的,他是先把老数组复制到新数组里来,大小为len + 1,接着是对新数组进行更新操作
把新数组的最后一位的元素设置成要添加的元素,你的更新的操作此时都是发生在新数组里的,跟老数组是没关系的,写时复制的机制,写数据的时候,复制一个新的数组,然后在新的数组里更新元素
最后再把新的数组设置为CopyOnWriteArrayList对应的一个数组,volatile写保证说,只要他一写,其他线程可以立马读到
老数组稍后就会被jvm垃圾回收掉了,已经没有人使用他了
ini
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
将老数组复制到一个新数组里去,新老两个数组的大小是一样的,都是len,修改一个元素,并不是删除元素,也不是新增元素
对复制之后的一个新数组的指定index位置的元素设置为element元素
他就会将修改后的新数组设置为CopyOnWriteArrayList底层的数组
ini
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
每一个CopyOnWriteArrayList底层都对应一个数据结构,Object[]数组,同时还对应了一个ReentrantLock独占锁,就是用独占锁来保证说要修改Object[]数组的时候,必须加独占锁,此时只能有一个线程获取锁
独占锁保证了,只有一个线程可以来修改底层的数组里的数据
增删改操作的时候,都必须先获取一把ReentrantLock独占锁,保证同一时间只能有一个线程来操作底层的数组数据结构,更新CopyOnWriteArrayList的多线程并发的安全性就被保证了,多线程并发写的时候
并发写CopyOnWriteArrayList的性能是较差的,基本上所有的线程都需要串行起来写CopyOnwriteArrayList,一个线程先写完,下一个线程才能写
csharp
public E get(int index) {
return get(getArray(), index);
}
css
private E get(Object[] a, int index) {
return (E) a[index];
}
写数据的时候一定要CopyOnWrite,如何解决读写并发的问题,写数据的时候,如何安全的读数据
CopyOnWriteArrayList设计思想,并不是基于CAS执行读写操作,写数据的时候,全部复制一个副本,新的数组,对新的数组来修改,修改好了设置回去就可以了。读数据,只有两种情况
第一种:我读到的老数组的数据
第二种:其他线程更新好了数组,volatile写,我读到的是新数组的数据
我不需要依赖任何一种加锁的机制来保证数据读写并发的安全性,我甚至都不需要依赖于Unsafe.getObjectVolatile(),volatile读机制,来读取数组里的元素,我直接就是最最简单,最最高效
最最高性能的,读就是直接读当前数组的数据即可,要么读的是老数据,要么读的是最新的数据,都有可能
写数据更新的是复制好的另外一个副本数组,同一时间大量的线程读数据的时候,都是在读老数组的数据,读写之间是没有任何的并发冲突问题的,读和写之间是没有锁的冲突的,写的是副本数组
CopyOnWrite机制,写副本数组,跟读就没关系了,只要写完成之后,走一个volatile写,设置最新的数组,自然读操作就会读到最新数组的元素了,只有一个线程可以写,但是写的同时可以允许大量的线程来并发读
vbnet
List<String> list = new CopyOnWriteArrayList<String>();
list.add("张三");
list.set(0,"李四");
list.remove(0);
list.get(0);
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
csharp
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
arduino
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code set}
* is not supported by this iterator.
*/
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code add}
* is not supported by this iterator.
*/
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
Iterator迭代器里面是包含了一个snapshot,指向的就是CopyOnWriteArrayList当前的数组,array对象,你创建了一个Iterator迭代器对象之后,在迭代的过程中,都是基于快照数组在进行遍历的
如果此时有另外一个线程更新了数组的元素,设置了array,但是此时对迭代是没有任何影响的,你的迭代器里有一个snapshot指针指向了老的数组对象,他会一直用这个老的数组完成遍历
迭代也是基于快照的机制来实现的,读操作其实跟写操作都是半毛钱关系没有的,CopyOnWrite机制,这样的一套设计思想,保证了就是说你的读的并发能力是很强的,不需要加锁的
CopyOnWriteArrayList:弱一致性
多个线程并发的读写list,中间一定是有一段时间,是复制数组被修改好了,还没设置给array;但是此时其他线程读到的都是老数组的数据,这个过程中,多个线程看到的数据是不一致的,人家修改了数据没有立马被人读到
弱一致性 -> 最终一致性
优点:读和写不互斥的,写和写互斥,同一时间就一个人可以写,但是写的同时可以允许其他所有人来读;读和读也是并发的;读写锁机制还要好;他也不涉及到Unsafe.getObjectVolatile
使用场景:多线程并发安全性,可以选用他;尽可能是读多写少的场景,大量的读是不被影响的;可能有一个线程刚刚发起了写,此时别的线程读到的还是旧的数据,也有这种可能,还好
缺点:空间换时间,写的时候,经常内存里会出现复制出来的一模一样的副本,对内存消耗过大,副本机制保证了保证读写并发优化,大量的并发读不需要锁互斥,list如果很大,可能你要考虑在线上运行的时候,可能经常
内存占用会是list大小的几倍