概述
写时复制机制的ArrayList,可以保证线程并发的安全性
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
从上面这段构造函数的代码,就可以看出来,CopyOnWriteArrayList其实也是底层基于数组来实现的
private transient volatile Object[] array;
核心的底层数据结构是数组,volatile,保证 多线程读写的可见性,只要有一个线程修改了这个数组,其他的线程立马是可以读到的
final transient ReentrantLock lock = new ReentrantLock();
每一个CopyOnWriteArrayList底层都对应一个数据结构,Object[]数组,同时还对应了一个ReentrantLock独占锁,就是用独占锁来保证说要修改Object[]数组的时候,必须加独占锁,此时只能有一个线程获取锁.独占锁保证了,只有一个线程可以来修改底层的数组里的数据。
增删改操作的时候,都必须先获取一把ReentrantLock独占锁,保证同一时间只能有一个线程来操作底层的数组数据结构,更新CopyOnWriteArrayList的多线程并发的安全性就被保证了,多线程并发写的时候,写并发并不是特别的好。
ConcurrentHashMap分段加锁,CAS非阻塞式的分段加锁,分段的粒度很细,所以并发写的性能是特别好的,并发读是通过volatile读来保证一定是读到最新的数据的
并发写CopyOnWriteArrayList的性能是较差的,基本上所有的线程都需要串行起来写CopyOnwriteArrayList,一个线程先写完,下一个线程才能写
add
CopyOnWrite:写时复制的机制,大量的写操作都是基于写时复制的机制来实现的
Object[] newElements = Arrays.copyOf(elements, len + 1);
elements:代表的是当前CopyOnWriteArrayList内部的数组,Arrays.copyOf复制的操作,就是把当前数组复制到了新的数组里去,新的数组的长度是len + 1,newElements,就是一个全新的数组
新数组里包含了老数组所有的元素,而且长度还多了1位
CopyOnWrite,写数据的时候,不是直接在当前的数组里写的,他是先把老数组复制到新数组里来,大小为len + 1,接着是对新数组进行更新操作
ArrayList就不大一样了,先有一个固定大小的数组,往里面放数据,如果达到一定的程度,就会扩容这个数组
newElements[len] = e;
把新数组的最后一位的元素设置成要添加的元素,你的更新的操作此时都是发生在新数组里的,跟老数组是没关系的。写时复制的机制,写数据的时候,复制一个新的数组,然后在新的数组里更新元素
setArray(newElements);
最后再把新的数组设置为CopyOnWriteArrayList对应的一个数组,volatile写保证只要他一写,其他线程可以立马读到。
老数组稍后就会被jvm垃圾回收掉了,已经没有人使用他了
set
写时复制的机制
Object[] newElements = Arrays.copyOf(elements, len);
将老数组复制到一个新数组里去,新老两个数组的大小是一样的,都是len,修改一个元素,并不是删除元素,也不是新增元素
newElements[index] = element;
对复制之后的一个新数组的指定index位置的元素设置为element元素
setArray(newElements);
他就会将修改后的新数组设置为CopyOnWriteArrayList底层的数组
remove
比如说🈶️一个CopyOnWriteArrayList数组;
[ 张三, 李四, 王二, 麻子]
//len 数组长度
//idex 需要操作数据的索引位置
int numMoved = len - index - 1;
list.remove(1),要删除李四,步骤:
创建一个新的数组,新数组的大小是3
张三复制到新数组里去,把王二和麻子复制到新数组里去,新数组里就是[张三, 王二, 麻子]
//新数组的大小:len - 1,比老数组的大小少1
Object[] newElements = new Object[len - 1];
//把老数组里的,从 0开始的元素,复制到新数组的从 0 位置开始的地方。复制的个数是index
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
[null, null, null]
[张三, null, null]
[张三, 王二, 麻子]
再把新数组走一个volatile写,设置为CopyOnWriteArrayList底层的数组
复制数组参数的解释:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
src: 原数组
srcPos: 原数组起始位置(从这个位置开始复制)
dest: 目标数组
destPos:目标数组粘贴的起始位置
length: 复制的个数
如果要删除的是最后一个元素,此时是不需要移动任何元素的
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
[张三, 李四, 王二]
CopyOnWrite理解的透彻,增删改的时候,都是先复制一个数组出来,对新的复制数组进行修改,最后将修改好的新数组设置为底层数组
get读操作
list.get 写操作: 独占锁 + 写时复制
就是直接从底层数组里来读取数据,通过index定位对应位置的元素就可以了。
假设此时要add一个赵六进去
写数据的时候一定要CopyOnWrite,如何解决读写并发的问题,写数据的时候,如何安全的读数据,ConcurrentHashMap里面是直接操作一个数组的,对数组读写全部是走的volatile的操作
CopyOnWriteArrayList设计思想,并不是基于CAS执行读写操作,写数据的时候,全部复制一个副本,新的数组,对新的数组来修改,修改好了设置回去就可以了。读数据,只有两种情况
第一种:读到的老数组的数据
第二种:其他线程更新好了数组,volatile写,读到的是新数组的数据
不需要依赖任何一种加锁的机制来保证数据读写并发的安全性,甚至都不需要依赖于Unsafe.getObjectVolatile(),volatile读机制,来读取数组里的元素,直接就是最简单,最高效,最高性能的,读就是直接读当前数组的数据即可,要么读的是老数据,要么读的是最新的数据,都有可能。
写数据更新的是复制好的另外一个副本数组,同一时间大量的线程读数据的时候,都是在读老数组的数据,读写之间是没有任何的并发冲突问题的,读和写之间是没有锁的冲突的,写的是副本数组
CopyOnWrite机制,写副本数组,跟读就没关系了,只要写完成之后,走一个volatile写,设置最新的数组,自然读操作就会读到最新数组的元素了,只有一个线程可以写,但是写的同时可以允许大量的线程来并发读
CopyOnWriteArrayList:弱一致性
也可以理解为最终一致性
多个线程并发的读写list,中间一定是有一段时间,是复制数组被修改好了,还没设置给array;但是此时其他线程读到的都是老数组的数据,这个过程中,多个线程看到的数据是不一致的,人家修改了数据没有立马被人读到。
优点: 读和写不互斥的,写和写互斥,同一时间就一个人可以写,但是写的同时可以允许其他所有人来读;读和读也是并发的;读写锁机制还要好;他也不涉及到Unsafe.getObjectVolatile
使用场景:多线程并发安全性,可以选用他;尽可能是读多写少的场景,大量的读是不被影响的;可能有一个线程刚刚发起了写,此时别的线程读到的还是旧的数据。
ArrayList,synchronized(list),ReadWriteLock来操作这个ArrayList
缺点:空间换时间,写的时候,经常内存里会出现复制出来的一模一样的副本,对内存消耗过大,副本机制保证了保证读写并发优化,大量的并发读不需要锁互斥,list如果很大,可能要考虑在线上运行的时候,可能经常频繁GC;
内存占用会是list大小的几倍