多线程(5) — JDK的并发容器

  JDK提供了一些高效的并发容器,下面介绍几个

    • ConcurrentHashMap:这是个高效的并发HashMap,可以理解为一个线程安全的HashMap。
    • CopyOnWriteArrayList:这是一个List,从名字看就知道它和ArrayList是一族的,在读多写少的场合,这个List的性能非常好,远远优于Vector。
    • ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看作一个线程安全的LinkedList。
    • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口,表示阻塞队列,非常适合作为数据共享的通道。
    • ConcurrentSkipListMap:跳表的实现,这是个map,使用跳表的数据结构进行快速查找。

1. 线程安全的HashMap

  Collections.synchronizedMap()方法包装我们的HashMap,可以产生一个线程安全的HashMap。

public static Map map = Collections.synchronizedMap(new HashMap());

  Collections.synchronizedMap()产生一个SynchronizedMap的Map,它使用委托将自己所有Map相关的功能交给传入的HashMap实现,而自己主要负责保证线程安全。

private static class SynchronizedMap<K,V>
        implements Map<K,V>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;

        private final Map<K,V> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize

  SynchronizedMap里包装了一个map,通过mutex实现对这个map的互斥操作,实现线程的安全,其中的一些实现方法如下:

    public int size() {
            synchronized (mutex) {return m.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
        public boolean containsKey(Object key) {
            synchronized (mutex) {return m.containsKey(key);}
        }
        public boolean containsValue(Object value) {
            synchronized (mutex) {return m.containsValue(value);}
        }
        public V get(Object key) {
            synchronized (mutex) {return m.get(key);}
        }

        public V put(K key, V value) {
            synchronized (mutex) {return m.put(key, value);}
        }
        public V remove(Object key) {
            synchronized (mutex) {return m.remove(key);}
        }
        public void putAll(Map<? extends K, ? extends V> map) {
            synchronized (mutex) {m.putAll(map);}
        }
        public void clear() {
            synchronized (mutex) {m.clear();}
        }

  这个包装的Map可以满足线程安全的要求,但是在多线程的性能表现里不是很好,无论对map是读是写都要获得mutex锁,导致对map的操作全部进入等待状态,并发不高的情况下是可以的,如果高并发的话,我们可以使用另外一个类ConcurrentHashMap。这个的线程是绝对安全的,并且并发的效率还很高。

2. 线程安全的List

  Vector是线程安全的,而ArrayList和LinkedList不是线程安全的,可以使用Collections.synchronizedList()方法包裹任意List:

public static List list = Collections.synchronizedList(new ArrayList<String>());

  这样生成的List就是安全的了

3. 高效读写的队列:深度剖析ConcurrentLinkedQueue类

  这个队列使用链表作为数据结构,是高并发环境中性能最好的队列,线程安全完全是由CAS操作和队列的算法来保证的。作为一个链表,自然定义链表内节点,node如下:

private static class Node<E> {
        volatile E item;
        volatile Node<E> next;

  item是用来表示目标元素的。next表示下一个节点,这样就环环相扣了。对Node的进行操作时,使用CAS。

boolean casItem(E cmp, E val) {  //cmp是期望值,val是目标值,当前值等于cmp时会将目标值设置为val
    return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}

void lazySetNext(Node<E> val) {
     UNSAFE.putOrderedObject(this, nextOffset, val);
}

boolean casNext(Node<E> cmp, Node<E> val) {
     return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

  方法castItem()表示设置当前Node的item值,他需要俩参数,第一个值为参数的期望值,第二个参数是设置目标值,也就是当前值等于cmp期望值时就会将目标设置为val,同样casNext方法也是类似,但是用于next字段,而不是item字段。

  ConcurrentLinkedQueue类内部有两个重要的字段,head和tail,分别表示链表的头和尾,他们都是Node类型。对于head,不会是null,通过head和succ()后继方法一定能完整遍历整个链条。对于tail,表示队列的末尾。但是这个类在运行时允许链表处于多个不同状态,拿tail来说,tail的更新并不是及时的,可能产生拖延

  

    public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p 是最后一个节点
                if (p.casNext(null, newNode)) {
                    // CAS成功了的话,e会成为队列中的一个节点,newNode会活化
                    if (p != t) // 每两次更新一个tail
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // 竞争失败会再次尝试
            }
            else if (p == q)
                // 遇到哨兵节点从head开始遍历,但如果tail被修改,则使用tail(因为可能被修改正确了)
                p = (t != (t = tail)) ? t : head;
            else
                // 取下一个节点或者最后一个节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

  整个方法的核心是一个没有出口的 for 循环,直到尝试成功才会退出,这符合CAS操作流程。当第一个元素进来时,队列是空的,p.next()为null。将p的next节点赋值为newNode,也就是将新的元素加入队列中。此时p==t成立,不会执行更新tail末尾。如果casNext()方法成功,则程序直接返回,如果失败,则再进行一次循环尝试,直到成功,因此增加一个元素后,tail不会更新。

  当程序增加第2个元素时,由于t还在head的位置,p.next指向第一个元素,因此q不等于null不是最后的节点,于是程序会取下一个节点直到取到最后一个节点,此时它的next是null,故在下次循环时q==null是true,会更新自己next,如果更新成功,那么此时 p != t,会更新t所在位置,将t移动到链表的最后。

  p==q的情况其实当遇到哨兵节点时的处理,所谓哨兵节点就是next指向自己的节点,这种节点存在的意义不大,主要表示删除的节点或者空节点,因为通过next无法取到后续节点,所以直接返回head,从头开始遍历。一旦在执行的过程中发送tail被其他线程修改的情况,则进行一次打赌,使用新的tail作为链表的末尾。

  下面这段代码在理解上给说明一下:

p = (t != (t = tail)) ? t : head;

  首先“!=”并不是原子性操作,也是可以被中断的,也就是说,在执行“!=”时,程序会先取得 t 的值,再执行t=tail,再取得t的新值,然后比较这两个值是否相等。单线程环境下这个肯定不会成立,但是在高并发情况下,在获得左边的 t 后,右边的 t 被其他线程修改了,这样 t != t 就成立了。在比较过程中tail被其他线程修改,当再次赋值给t的时候,导致了左右不等了,这时候就用新的tail作为链表的尾部。

4. 高效读取:不变模式下的CopyonWriteArrayList类

  如果某个系统的读取操作很多,那么 每次读取都加锁势必会造成资源的很大浪费,因为读读之间不冲突的,但是写操作会阻塞读操作和写操作的。为了将读取的性能发挥到极致,CopyOnWriteArrayList类在读读之间,读写之间不加锁,只在写写之间加锁,这样性能就提升很多了,实现原理是:在写如操作时进行一次自我复制,也就是在List修改时,不修改原有内容而对数据进行一次复制,将修改的内容写入副本,再用修改后的副本替换原来的数据,这样就保证了写操作不会影响读。

  下面代码是读取的实现,可见读取操作没有任何同步控制和锁操作,原因是array不会发生修改,只会被另外一个array替换,因此可以保证数据安全。

private volatile transient Object[] array;
final Object[] getArray() {
    return 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];
}

  写入操作就比较麻烦一些了,加了锁操作,仅用于控制写-写情况。代码内部会对array完整复制,可生成一个新的数组newElements,将新元素加入新数组,再用这个新的替换老的数组,修改完成,整个过程不会影响读取,修改完成后,读取线程会立即察觉array的变化,因为array是volatile类型的。

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();
    }
}

5. 随机数据结构:跳表(SkipList)

  跳表可以用来快速查找的数据结构,类似平衡树。区别是:平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整而跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样在高并发情况下,平衡树需要一个全局锁保证安全,而跳表只要部分锁就可以了。这样跳表在高并发环境下性能会提高。跳表另外一个特点就是随机算法,其本质是同时维护多个链表,并且链表是分层的。

  跳表内所有元素都是排序的,查找时先从顶级链表开始找,一旦发现当前链表的取值所在范围就会进入下一层。比如上图中查找7的话,从顶层开始因此可以快速跳过小于7的,第二层8大于7,所以从6进入下一层,这样在第三层找到了7。此外跳表内所有元素都是有序的,实现这一数据结构类是ConcurrentSkipListMap。内部实现由几个关键的数据结构组成,首先是Node,一个Node一个节点,里面有key和value俩元素,每个Node还会指向下一个Node,因此还有个Next

static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;

对node操作使用CAS方法,CASValue()用来设置value的值,casNext()方法用来设置next字段。

boolean casValue(Object cmp, Object val) {
      return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
}
boolean casNext(Node<K,V> cmp, Node<K,V> val) {
      return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

另外一个重要的数据结构是index,顾名思义,这个索引内部包装Node,同时增加了向下和向右的引用。整个跳表就是根据Index进行全网组织的

static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;

此外,对于每一层的表头还需要记录当前处于哪一层,还需要一个HeadIndex的数据结构,表示链表头部的第一个Index,继承自Index

static final class HeadIndex<K,V> extends Index<K,V> {
    final int level;
    HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
      super(node, down, right);
      this.level = level;
   }
}
原文地址:https://www.cnblogs.com/wangyongwen/p/11253465.html