ArrayList 的使用方法
深入理解 ArrayList
深入理解 LinkedList
深入理解 Vector
fail-fast 与 fail-safe 的区别
CopyOnWriteArrayList 我们可以拆分为两个词来理解,一个是 CopyOnWrite,一个是 ArrayList,ArrayList 相信 使用 Java 的朋友都知道。那么什么是 CopyOnWrite 呢?
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源
在说 CopyOnWriteArrayList 的实现原理之前,我们先来看下下面这段代码
package juc;
import java.util.*;
/**
* @author Woo_home
* @create by 2020/3/7
*/
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
输出:
当我们并发地去修改 ArrayList 集合中的内容的时候会抛出 ConcurrentModificationException 异常,因为当我们并发地去修改的时候,ArrayList 会执行 checkForComodification 这个方法
protected transient int modCount = 0;
int expectedModCount = modCount;
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hasNext() 或者 next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;否则抛出异常,终止遍历
package juc;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author Woo_home
* @create by 2020/3/7
*/
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
// 在 list 中添加 UUID 类随机生成的字符串
list.add(UUID.randomUUID().toString().substring(0,8));
// 打印
System.out.println(list);
// 启动三十个线程
},String.valueOf(i)).start();
}
}
}
输出:
可以看到程序是可以正常运行的,那么 CopyOnWriteArrayList 是如何做到数据的一致性的呢?
通过源码我们可以发现 CopyOnWriteArrayList 底层是一个 Object 的数组,而我们上面 add() 源码中 getArray() 方法也是返回一个 array 的 Object 数组。而且我们可以发现 array 这个数组是使用 volatile 修饰的,关于 volatile 的说明请看这里 volatile 与 synchronized 详解 ,而且 array 这个数组只提供给 setArray 和 getArray 方法访问
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
我们来看下 CopyOnWriteArrayList 的底层是如何实现的
public boolean add(E e) {
// 初始化 ReentrantLock
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;
// 将array引用指向到新数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
在 add() 方法中,有一个 Arrays.copyOf(elements, len + 1); 这段代码的是将原有的集合复制一份出来,然后我们遍历的其实是这个复制的集合而不是原有的集合,这有修改的就不是原有的集合,而是复制的集合,遍历完成之后再复制给新的数组