欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

jdk容器-安全失败机制

程序员文章站 2024-03-06 08:57:43
...

在上篇文章 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());
    }	

运行结果:

jdk容器-安全失败机制

可以看到,在迭代集合的过程中,移除元素,没有任何异常发生。这就是我们今天要讲的 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