前言
我们所知 Map 双列集合的 Hashtable 和 Collection 单列集合的 Vector 是线程安全的,但是这两种方式都是简单粗暴的使用 synchronized 对涉及方法进行加锁,效率很低下。所以 Java 并发包中对 HashMap 提出了一种效率更高的一种线程安全的方式,也就是 ConcurrentHashMap,并且在 Jdk8 中性能得到了再次提升。
而对于 List 和 Set,Java 并发包也提出了一种解决方式,就是 CopyOnWriteArrayList 和 CopyOnWriteArraySet。它们采取一种读写分离的并发策略,我们也可以称这种容器为“写时复制器”,即读数据是一种无锁操作,写数据会将当前容器复制一份,添加或删除时都是对新容器中的元素进行操作,读数据依旧是老容器,只有在添加或删除完成时,对象引用指向新生成的容器。
这种方式的优缺点显而易见,对于读多写少的场景非常适用,也不会出现并发修改异常 ConcurrentModificationException,缺点就是占用内存,频繁 GC,不能保证读和写的强一致性。
源码
下面是 Jdk8 的 CopyOnWriteArrayList 源码,很容易看懂:
1)get 方法是在无锁的状态进行:
public E get(int index) {
return get(getArray(), index);
}
2)add 过程会加锁:
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();
}
}
3)remove 同样:
public E remove(int index) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 原容器
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
// 删除元素的索引位置正好时数组的最后一位,则直接 copy 此索引位置之前的元素即可
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
// 删除元素的索引位置是其他位置,分成两部分 copy
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
其实 Java 并发包对于 List 和 Set 提出的这种解决方式,没有太多的从技术上解决并发问题,我们在使用时也要针对不同场景选择合理的一种解决方式即可。