线程安全一直是编程方面最为注意的一个点,今天就来聊聊关于线程安全类CopyOnWriteArrayList是如何实现线程安全的。
关于保证线程安全,最常规的做法也就是加锁,但是加锁势必会导致性能方面的下降,这是无法避免的,那么有没有什么好的办法在尽可能保证性能的情况下加锁呢,接下来看看CopyOnWriteArrayList源码。
通过源码发现CopyOnWriteArrayList底层也是通过一个数组保存数据的:
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全
public boolean add(E e) {
//获取锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取到当前List集合保存数据的数组
Object[] elements = getArray();
//获取该数组的长度(这是一个伏笔,同时len也是新数组的最后一个元素的索引值)
int len = elements.length;
//将当前数组拷贝一份的同时,让其长度加1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将加入的元素放在新数组最后一位,len不是旧数组长度吗,为什么现在用它当成新数组的最后一个元素的下标?建议自行画图推演,就很容易理解。
newElements[len] = e;
//替换引用,将数组的引用指向给新数组的地址
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
再来看看读操作:
public E get(int index) {
return get(getArray(), index);
}
总结:只在写操作做了加锁处理,但是读操作并没有做任何的加锁处理。那么问题来了,这样不就会导致线程不安全吗?答案是并不会导致线程不安全。因为读是没有加锁的,所以读是一直都能读,但是在写的时候,看到源码可以知道写入新元素时,首先会先将原来的数组拷贝一份并且让原来数组的长度+1后就得到了一个新数组,新数组里的元素和旧数组的元素一样并且长度比旧数组多一个长度,然后将新加入的元素放置都在新数组最后一个位置后,用新数组的地址替换掉老数组的地址就能得到最新的数据了。在我们执行替换地址操作之前,读取的是老数组的数据,数据是有效数据;执行替换地址操作之后,读取的是新数组的数据,同样也是有效数据,而且使用该方式能比读写都加锁要更加的效率。