jdk容器-安全失败机制
在上篇文章 jdk容器-快速失败机制中,我们提到 jdk 为非同步容器建立的一种安全警报机制,通过 fail-fast 通知用户可能存在线程安全问题,本质上是因为不同线程并发访问同一集合对象导致的。
在主题开始前,我们先来看一段测试代码,和昨天模拟快速迭代失败的代码大体一致,只不过替换了容器。
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for (String s : list) {
System.out.println(s);
list.remove(s);
}
System.out.println("list size :" + list.size());
}
运行结果:
可以看到,在迭代集合的过程中,移除元素,没有任何异常发生。这就是我们今天要讲的 fail-safe——安全失败机制,而 CopyOnWriteArrayList 正是实现了这种机制来预防线程安全问题。
fail-safe
在 jdk 的定义中其实没有 fail-safe 的定义,本质上是用来区分 fail-fast 而衍生出的一种说法,它与 fail-fast 的区别在于:
- fail-safe 迭代器允许在遍历集合的同时,修改集合的结构,且不会抛出类似 ConcurrentModificationException 的异常
- fail-safe 迭代器会创建原始集合的副本,在新副本的基础上进行修改操作,因此需要额外申请内存空间
fail-safe 最经典的实现便是 COW(copy on write),即写时复制,另外我们可以猜想:COW 采用的是一种类似『懒加载』的思想,即只有在进行修改操作时才会发生集合的复制。
写时复制
从名字上就能看出 CopyOnWriteArrayList 集合是通过写时复制实现 fail-safe 迭代器,下面我们就来看一下具体实现,顺便验证一下猜想。
实现原理
首先看到,CopyOnWriteArrayList 内部迭代器的主要代码。
static final class COWIterator<E> implements ListIterator<E> {
// 数组的快照
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
代码非常简洁,与 ArrayList 迭代器不同的是:
- 在创建迭代器的同时,其中的成员变量 snapshot 会指向当前集合的数组对象
- 在使用 next() 方法遍历集合的时候,并没有 checkForComodification 的检查
小伙伴可能有疑问了,啥检查没有,CopyOnWriteArrayList 怎么解决线程安全的问题?我这边也不废话,直接步入主题,看到 CopyOnWriteArrayList 的 remove() 方法。
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
// 省略索引检查相关的代码,不是本文讨论的重点
Object[] newElements = new Object[len - 1];
// 创建了一个当前集合数组的副本 newElements
System.arraycopy(current, 0, newElements, 0, index);
// 在副本 newElements 的基础上对元素进行修改操作
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
// 最后用修改后的副本 newElements 覆盖原始集合数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
步骤如下:
- 创建了一个当前集合数组的副本 newElements
- 在副本 newElements 的基础上对元素进行修改操作
- 最后用修改后的副本 newElements 覆盖原始集合数组(改变引用指向)
现在明白迭代器中那个叫做 snapshot 变量命名的意义了吧,因为它一旦被创建,就不会再改变了,可以认为是那个时刻集合元素的快照,任何涉及到集合元素改变的操作,都是在新的集合副本中进行,最后再让集合数组引用指向新的集合副本,好一个『偷天换日』。
但需要注意,在拷贝副本时还是进行了加锁操作,防止多个线程同时创建多份副本,如果那样还是线程不安全;另外写时复制只能保证数据的『最终一致性』。
设计思想
参考《Java 并发编程实战》一书。
Copy-On-Write 容器的线程安全性在于,只要正确地发布一个事实上不可变的对象,那么在访问该对象时就不再需要额外进行同步操作。
相对同步容器,写时复制在保证线程安全性的同时,也提高了并发读取的性能。但由于写操作需要复制操作,存在一定开销,特别是当容器的规模较大时,这种性能消耗更加明显,因此它更适用于『读多写少』的场景。
总结
通过本文,我们主要了解了:
- 什么是 fail-safe ?
- Copy-On-Write 写时复制的实现
- Copy-On-Write 的设计思想,以及适用场景
如果觉得文章对你有帮助,欢迎留言点赞。
上一篇: 使用Java设置字型和颜色的方法详解