Java线程之JUC中的常用线程安全集合类
一、ConcurrentHashMap
二、BlockingQueue(暂时忽略)
三、ConcurrentLinkedQueue(暂时忽略)
四、CopyOnWriteArraylist
1. 基本概念
CopyOnWriteArraylist
底层实现采用了写入时拷贝的思想,即增删改操作会将底层数组拷贝一份,在新数组上执行这些操作,这时不影响其它线程的并发读(因为读的是旧数组)。它实现了读写分离。比较适合读多写少的场景。CopyOnWriteArraySet
是CopyOnWriteArraylist
的马甲,它的源码中实际上是定义了一个CopyOnWriteArraylist
对象作为其属性,在调用CopyOnWriteArraySet
的方法时,底层实际上调用的都是CopyOnWriteArraylist
的方法,CopyOnWriteArraySet
的存储元素不重复的实现也是通过再添加元素时,调用CopyOnWriteArraylist
的addIfAbsent(E e)
方法判断内部是否已经存在当前要加入的元素,如果不存在,才进行添加。
2. 源码分析
2.1 更改(以JDK8新增为例)
加锁是为了保证写写的互斥,不能同时进行写操作。但是读取是不影响的,可以并发执行,只不过读的是旧数组而已。
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
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();
}
}
final void setArray(Object[] a) {
array = a;
}
注意:JDK8和JDK11有所不同,不同的是JDK8中使用的是ReentrantLock而JDK11中使用的是synchronized。
2.2 读(JDK8)
读操作是未加锁的。
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
3. 弱一致性
3.1 读操作的弱一致性
读操作指的是foreach遍历操作和get操作,下面仅以get操作为例进行说明。
- Thread-0想要读取
CopyOnWriteArraylist
中下标为0的数据,获得了CopyOnWriteArraylist
内部的属性array的引用,此时,仅仅获取了数组的引用,还未来的及读取数据。 - Thread-1想要对数组下标为0的元素进行删除,复制了一个新的数组,并删除了下标为0的元素,将新的数组的引用设置为
CopyOnWriteArraylist
内部的属性array的引用。 - 此时尽管下标为0的元素已经删除了,且新的引用已经赋给了
CopyOnWriteArraylist
内部的属性array,但是Thread-0拿到的还是原来的引用,因此还可以读到原来的下标为0的数据。
3.2 迭代器的弱一致性
再利用迭代器进行遍历的情况下,同样会存在上述的问题。
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}
4. 注意
- 并不要觉得弱一致性就不好,它可以做到读写的并发,数据库的 MVCC 都是弱一致性的表现。
- 并发高和一致性是矛盾的,需要权衡。