通过学习,我们可以发现,ArrayList 作为共享变量的话,是线程不安全的。
如果要想保住线程安全,可以通过以下两种方式实现:
1、Collections.synchronizedList 方法
2、 CopyOnWriteArrayList 方法
今天记录一下:CopyOnWriteArrayList 的学习
CopyOnWriteArrayList 方法:
- 线程安全的,多线程环境下可以直接使用,底层在读时不会加锁,写时会加锁;
- 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
- 写时复制,每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。
1、整体架构:
从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过
CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁;
- 从原数组中拷贝出新数组;
- 在新数组上进行操作,并把新数组赋值给数组容器;
- 解锁。
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修
改,其它线程立马能够感知到,代码如下:
private transient volatile Object[] array;
2、CopyOnWriteArrayList 新增元素:
新增也分多组情况:尾部新增、指定位置新增、批量元素新增
下面看下尾部新增源码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//得到所有的原数组
Object[] elements = getArray();
int len = elements.length;
//拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
//替换掉原来的数组
setArray(newElements);
return true;
} finally {
// finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁
lock.unlock();
}
}
上面 add 方法 过程都是在持有锁的状态下进行的,通过加锁,来保证同一时刻只能有一个线程能够对同一个数组进行 add 操作。除了加锁之外,还会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组上,这时候就有一个问题:都已经加锁了,为什么需要拷贝数组,而不是在原来数组上面进行操作,原因主要是:
volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
3、总结:
从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:
- 加锁:保证同一时刻数组只能被一个线程操作;
- 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马
知道数组已经被修改; - volatile:值被修改后,其它线程能够立马感知最新值。