并发容器

并发容器

Posted by bulingfeng on August 2, 2024

简介

任何一个java的容器都可以使用Collections类提供的synchronizedXXX()函数来把对应的容器转换为线程安全的容器,但是这种加锁是简单粗暴的,性能比较低下。

而JUC(java util concurrent)提供的一些并非容器,虽然不能和java的容器一一对应,但是由于使用了分段锁,所以可以高效的执行代码。

写时复制

当多线程写一个容器的时候(多个增,删,改操作一起的时候),就会造成线程安全问题。于是可以把原始的数据给复制一份,然后老的数据用来进行查看,新的一份数据供增,删,改,经过写操作以后,然后再把新容器代替老容器。也就是相当于把读的操作和写的操作分开来避免线程安全问题,因为读操作的时候不会出现访问冲突等问题。

CopyOnWriteArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;
    /**
     * The lock protecting all mutators.  (We have a mild preference
     * for builtin monitors over ReentrantLock when either will do.)
     */
    final transient Object lock = new Object();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
    // ...
}

下面分别从增删改查来分析

1
2
3
4
5
6
7
8
9
10
11
// 添加
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 删除
public E remove(int index) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        E oldValue = elementAt(es, index);
        int numMoved = len - index - 1;
        Object[] newElements;
        if (numMoved == 0)// 是否是删除的最后一个
            newElements = Arrays.copyOf(es, len - 1);
        else {// 删除的非最后一个
            newElements = new Object[len - 1];
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index + 1, newElements, index,
                             numMoved);
        }
        setArray(newElements);
        return oldValue;
    }
}
1
2
3
4
// 查看
public E get(int index) {
    return elementAt(getArray(), index);
}

可以根据上面的源码得到结论:

  • 查看的时候和写的时候并没有什么关系,是完全独立开来的。
  • 数据是弱一直性的;
  • 适用的场景是读多写少的情况,因为每次写都会整个复制数组。

CopyOnWriteArraySet

和CopyOnWriteArrayList的实现基本一致,底层是利用CopyOnWriteArrayList来实现的。其中contains()函数基于数组来实现,需要遍历查询,执行效率比较低