一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
CopyOnWriteArrayList 源码分析-
在 ArrayList 的类注释上,记录了 ArrayList 是线程不安全的。 如果要作为共享变量的话,需要自己对方法进行加锁,或者使用Collections.synchronizedList 方法。另外,java中还有一种线程安全的List,是CopyOnWriteArrayList。
是CopyOnWriteArrayList的特点:
- 线程安全,多线程环境下无需加锁可以直接使用
- 底层通过锁 + 数组拷贝 + volatile 保证线程安全
CopyOnWriteArrayList 在对数组进行操作的时候,基本上是四个步骤:
- 加锁
- 从原数组中拷贝出新数组
- 在新数组上进行操作,并把新数组赋值给数组容器
- 解锁
CopyOnWriteArrayList 的底层数组被 volatile 关键字修饰
private transient volatile Object[] array;
一旦数组被修改,其它线程立马能够感知到。
整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。
从 CopyOnWriteArrayList 的类注释上还可以知道:
- 所有的操作都是线程安全的
- 数组的拷贝虽然有一定的成本
- 迭代过程中,不会影响到原来的数组,也不会抛出 ConcurrentModificationException 异常。
尾部新增
添加元素到数组尾部:
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();
}
}
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 修饰,要触发可见性,必须通过修改数组的内存地址才行。
指定位置新增
源码:
public void add(int index, E element) {
synchronized(this.lock) {
Object[] es = this.getArray();
int len = es.length;
if (index <= len && index >= 0) {
int numMoved = len - index;
Object[] newElements;
if (numMoved == 0) {
newElements = Arrays.copyOf(es, len + 1);
} else {
newElements = new Object[len + 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index, newElements, index + 1, numMoved);
}
newElements[index] = element;
this.setArray(newElements);
} else {
throw new IndexOutOfBoundsException(outOfBounds(index, len));
}
}
}
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 来保证了线程安全。