4.3 JDK并发数据结构

由于并行程序与串行程序的不同特点,适用于串行程序的一些数据结构可能无法直接在并发环境下工作,这是因为这些数据结构不是线程安全的。

 4.3.1 并发List

  Vector或者CopyOnWriteArrayList是两个线程安全的List实现,ArrayList不是线程安全的,因此,应该尽量避免在多线程环境中使用ArrayList。如果因为某些原因必须使用的,则需要使用下面代码进行包装。

Collections.synchronizedCollection(new ArrayList<>());

在没有对象进行写操作前,由于对象未发生改变,因此不需要加锁。而在试图改变对象时,总是先获取对象的一个副本,然后对副本进行修改,最后将副本写回。

 这种实现方式的核心思想是减少锁竞争,从而提高在高并发时的读取性能,但是它却在一定程度上牺牲了写的性能。

public E get(int index) {
        return get(getArray(), index);
    }

 Vector使用了同步关键字,所有的get()操作都必须先取得对象锁才能进行。在高并发的情况下,大量的锁竞争会拖累系统性能。

public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

 虽然CopyOnWriteArrayList的读操作性能优越,但是,它的写操作却不尽人意,它的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();
        }
    }

在每一次的add方法中,都是进行一次自我复制,同时,add也申请了锁,并不像get方法那样。相对地,Vector的add方法则要快捷很多。因此,在高并发且以读为主的应用场景中,CopyOnWriteArrayList要优于Vector,但是,当写操作为主的情况下,应该优先使用Vector.

CopyOnWriteArrayList如何做到线程安全的

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。当元素在新数组添加成功后,将array这个引用指向新数组。CopyOnWriteArrayList的整个add操作都是在的保护下进行的。 
这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

4.3.2 并发Set

   和List相似,并发Set也有一个CopyOnWriteArraySet,它实现了Set接口,并且是线程安全的。它的内部实现完全依赖于CopyOnWriteArrayList,因为他们的特性完全一致。

4.3.3 并发Map

由于Map是使用相当频繁的一个数据结构,因此JDK中便提供了一个专门用于高并发的Map实现CuncurrentHashMap。

4.3.4 并发Queue

一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue接口为代表的阻塞队列。不论哪种实现,都继承自Queue接口。

BlockingQueue的主要功能并不是在于提高高并发的队列性能,而在于简化多线程间的数据共享,它的性能低于COncurrentLinkedQueue。

4.3.4 并发Dueue

  在JDK中,还提供了一种双端队列,简称Depue。它允许在队列的头部或者尾部进行出队和入队操作。与Queue相比,它们具有更加复杂的功能。

LinkedBlockingDueue是一个线程安全的双端队列实现。可以说,它已经是最为复杂的一个队列实现。在内部实现中,LinkedBlockingDueue使用链表结构。每一个队列节点都维护一个前驱节点和一个后驱节点。LinkedBlockingDueue没有进行读写锁的分离,因此同一个时间只能有一个线程对其进行操作。因此在高并发的应用中,它的性能表现要远远低于LinkedBlockingQueue,更要低于CocurrentLinkedQueue。