CopyOnWriteArrayList 源码分析 - 基础和新增
CopyOnWriteArrayList 源码分析-
在 ArrayList 的类注释上,记录了 ArrayList 是线程不安全的。如果要作为共享变量的话,需要自己对方法进行加锁,或者使用 Collections.synchronizedList 方法。另外,java 中还有一种线程安全的 List,是 CopyOnWriteArrayList。
是 CopyOnWriteArrayList 的特点:
线程安全,多线程环境下无需加锁可以直接使用
底层通过锁 + 数组拷贝 + volatile 保证线程安全
CopyOnWriteArrayList 在对数组进行操作的时候,基本上是四个步骤:
加锁
从原数组中拷贝出新数组
在新数组上进行操作,并把新数组赋值给数组容器
解锁
CopyOnWriteArrayList 的底层数组被 volatile 关键字修饰
一旦数组被修改,其它线程立马能够感知到。
整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。
从 CopyOnWriteArrayList 的类注释上还可以知道:
所有的操作都是线程安全的
数组的拷贝虽然有一定的成本
迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常。
尾部新增
添加元素到数组尾部:
lock.lock();
加锁
Object[] elements = getArray();
得到所有的原数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素
newElements[len] = e;
在新数组中进行赋值,新元素直接放在数组的尾部
setArray(newElements);
替换掉原来的数组
finally { lock.unlock(); }
finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁
新增总结:整个 add 过程中都是在有锁的状态下进行的,保证了同一时刻只能有一个线程能够对同一个数组进行 add 操作。
除了加锁外,还会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组上。因为数组经过了 volatile 修饰,要触发可见性,必须通过修改数组的内存地址才行。
指定位置新增
源码:
int numMoved = len - index;
len:数组的长度、index:插入的位置
if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1);
如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); }
如果要插入的位置在数组的中间,就需要拷贝 2 次,第一次从 0 拷贝到 index,第二次从 index+1 拷贝到末尾
newElements[index] = element;
index 索引位置的值是空的,直接赋值即可。
setArray(newElements);
把新数组的值赋值给数组的容器中
总结
尾部新增和指定位置新增,可以看到 CopyOnWriteArrayList 中都通过了 加锁 + 数组拷贝+ volatile 来保证了线程安全。
评论