上篇推荐:Java中快速失败 (fail-fast) 机制
CopyOnWriteArrayList简介
CopyOnWriteArrayList是java.util.concurrent包下提供的一个线程安全
的ArrayList。它通过一个简单的策略来保证线程安全:当我们需要修改列表时(增加、删除、修改等操作),而不是直接对当前的内容进行操作,它会将当前的内容复制一份,在副本上执行修改
,然后将原列表指向新的副本
。
源码分析与加锁机制
CopyOnWriteArrayList使用了ReentrantLock来实现线程安全的修改操作。下面是一个简化版的源码展示其加锁机制:
private transient volatile Object[] array;
final transient ReentrantLock lock = new ReentrantLock();public boolean add(E e) {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;setArray(newElements);return true;} finally {lock.unlock();}
}
如上述代码所示,每一个修改操作都是在加锁的情况下执行的。首先复制当前的数组,然后在副本上执行修改,最后再将原来的数组引用指向新的副本。重要的是,获取数组的当前状态不需要加锁,因为不涉及修改操作。
增删查改实现
增加
如前所示,增加操作首先复制出一个新的数组,然后在新数组的末尾添加新的元素,并将原数组指向这个新数组。
删除
删除操作也是首先复制数组,然后从复制出的新数组中移除指定的元素,完成后,再将原数组指向新数组。
查找
查找操作是CopyOnWriteArrayList中效率最高的操作,因为它可以直接访问底层数组,无需加锁:
public E get(int index) {return get(getArray(), index);
}private E get(Object[] a, int index) {return (E) a[index];
}
修改
修改操作和增加、删除操作类似,首先是数组的复制,然后在新数组上修改指定位置的元素值,之后再将原数组指向新数组。
适用场景
CopyOnWriteArrayList适合读多写少
的并发场景。由于它的读取操作不需要加锁,可以充分利用硬件和操作系统的优化,如缓存一致性协议,使得读取操作非常高效。而写操作,由于涉及到复制整个数组,所以在数据量大、写操作频繁的场景中性能会相对较低。
问题
- 内存消耗:由于写操作需要复制整个数组,对于大规模数据的列表,这可能导致相当昂贵的内存消耗。
- 数据一致性:CopyOnWriteArrayList保证最终一致性而非实时一致性。在写操作发生的同时,读取操作可能仍然读到旧的数据。
- 迭代器弱一致性:迭代器不会反映出在迭代器创建之后的修改。
迭代器弱一致性 举例说明
public static void main(String[] args) {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("a");list.add("b");list.add("c");Iterator<String> iterator = list.iterator();// 在迭代进行中对列表进行修改list.add("d");list.remove("b");while (iterator.hasNext()) {System.out.println(iterator.next());}}
a
b
c
在迭代的过程中,迭代器只能看到最开始的快照,可能会错过后来添加或删除的元素。因此,虽然不会抛出异常,但迭代的结果可能是一个旧的快照,从而出现了迭代的弱一致性。
总的来说,CopyOnWriteArrayList是一种读写分离的思路,在特定的适用场景下可以提供很好的并发性能和线程安全性。然而,开发者在使用时需要注意它的限制和适用的局限性,以确保应用程序的性能和正确性。