JDK并发容器初步认识

来源:互联网 发布:爱动体感运动机 知乎 编辑:程序博客网 时间:2024/05/22 13:08

JDK为我们提供了很好的线程安全的容器,包括以下几种:
ConcurrentHashMap:是一个高效并发的HashMap。可以理解为一个线程安全的HashMap.
CopyOnWriteArrayList:是一个并发的list,在读多写少的场合下,性能远远好于Vector.
ConcurrentLinkedQueue:高效的并发队列,使用链表实现,可以看做一个线程安全的LinkedList。
BlockingQueue:这是一个接口,JDK内部通过链表,数组等方式实现了这个接口,表示阻塞队列,适合用于作为数据的共享通道。
ConCurrentSkipListMap:跳表实现。是一个Map,利用跳表的数据结构进行快速查找。

1.实现的方式
1.1 ConcurrentHashMap
ConcurrentHashMap是减小锁粒度的一种典型实现,对于ConcurrentHashMap,它内部进一步细分了若干个小的hashMap,称之为段。缺省情况下它被进一步分为16个段。
如果需要添加一个表项。并不是将整个Map加锁,而是根据hashcode得到该表项应该被存放到哪个段中,然后对该段加锁。如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程之间便可以做到并行操作。下面来看它的put和get方法:

  public V put(K key, V value) {        Segment<K,V> s;        if (value == null)            throw new NullPointerException();  //判断是否为空        int hash = hash(key);        int j = (hash >>> segmentShift) & segmentMask;        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment            s = ensureSegment(j);        return s.put(key, hash, value, false);    }

在第5~6行,根据key,获得对应的段的序号。接着在第9行,得到段,然后插入数据。在第3~4行可知,ConcurrentHashMap和HashTable一样,都是不可以用null值作为key或者value的,而HashMap可以。get方法:

 public V get(Object key) {        Segment<K,V> s; // manually integrate access methods to reduce overhead        HashEntry<K,V>[] tab;        int h = hash(key);        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&            (tab = s.table) != null) {            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);                 e != null; e = e.next) {                K k;                if ((k = e.key) == key || (e.hash == h && key.equals(k)))                    return e.value;            }        }        return null;    }

get方法和put的方法一样,先根据key获得对应的段号,然后再到对应的段取值。
1.2 CopyOnWriteArrayList
在很多的应用场景,读操作可能会远远大于写操作,这时我们可以用ReadWriteLock(读写锁),读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。读写锁在读读之间不阻塞,但是读写和写写之间会阻塞。但是在CopyOnWriteArrayList类中,写入也不会阻塞读取操作,只有写写之间需要进行同步等待。
实现原理:在写入操作时,list进行一次自我的复制,换句话说,当这个List需要修改时,我并不修改原有的内容,而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后再将修改完的副本替换原来的数据。这样就可以保证写操作不会影响读了。写操作代码:

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

通过代码可知,在多线程的情况下,写线程并不会影响读线程,因为array变量是volatile类型的。
1.3 ConcurrentLinkedQueue
ConcurrentLinkedQueue是在高并发环境中性能最好的队列,之所它的性能高,是因为它内部都是无锁操作,使用了CAS(比较交换)。这样就可以减少对锁的申请等操作,释放了系统的资源。它的节点定义如下:

volatile E item;volatile Node<E> next;

item表示目标元素,next表示当前node的下一个元素。在对node操作时,使用了CAS操作

boolean casItem(E cmp, E 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);        }

方法casItem()表示设置当前Node的item值,它需要两个参数,第一个参数为期望值,第二个参数设置目标值。当当前值等于cmp期望值时,就会将目标设置为val。同样casNext()也是类似,用来设置next字段。
ConcurrentLinkedQueue有两个重要的字段:head和tail。分别表示链表的头部和尾部。对于head来说永远不会为null。一般来说我们期望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) {                //CAS操作                if (p.casNext(null, newNode)) {                    if (p != t)                     //CAS操作                        casTail(t, newNode);                      return true;                }            }            else if (p == q)            //处理哨兵(自己指向自己的节点)节点,自己指向自己的节点                p = (t != (t = tail)) ? t : head;            else                //获取p.next的值然后赋p                p = (p != t && t != (t = tail)) ? t : q;        }    }

通过上述代码可知不使用锁而单纯的使用CAS会要求在应用层面保证线程安全,到那时程序设计的难度会大大增加。
1.4 BlockingQueue
BlockingQueue是一个接口,是数据共享的通道。它的实现类有如下:ArrayBlockingQueue,DealyedWorkQueue,DelayQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue,BlockingDeque。
那么BlockingQueue是怎么实现数据共享的呢,我们以ArrayBlockingQueue来一探究竟。向队列压入元素可以使用offer()方法和put方法。对于offer()方法,如果当前队列已经满了,它就会立即返回false,put方法也是将元素压入队列,但如果队列满了,他会一直等待,直到队列中有空闲的位置。从队列中弹出元素可以使用poll()方法和take()方法。不同之处在于:如果队列为空poll()方法会返回null,而take()方法会等待,直到队列中有可用的元素。
为了做好等待和通知两件事,定义了以下字段:

    final ReentrantLock lock;    private final Condition notEmpty;    private final Condition notFull;

当执行take()操作时,如果队列为空则让当前线程等待在notEmpty上。新元素入队时,则进行异常notEmpty上的通知。take()过程:

public E take() throws InterruptedException {        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == 0)                notEmpty.await();            return extract();        } finally {            lock.unlock();        }    }

如果队列为空,在第6行时会让当前线程等待。当有元素插入时线程会得到一个通知,insert代码:

private void insert(E x) {        items[putIndex] = x;        putIndex = inc(putIndex);        ++count;        notEmpty.signal();    }

在第5行操作会通知等待在notEmpty上的线程。
同理,对于put操作也是一样,当队列满时,需要压入线程等待,put代码:

 public void put(E e) throws InterruptedException {        checkNotNull(e);        final ReentrantLock lock = this.lock;        lock.lockInterruptibly();        try {            while (count == items.length)                notFull.await();            insert(e);        } finally {            lock.unlock();        }    }

如果队列已满,在第7行会让当前线程等待。在一个元素被移动时会通知等待插入的线程。extract代码:

    private E extract() {        final Object[] items = this.items;        E x = this.<E>cast(items[takeIndex]);        items[takeIndex] = null;        takeIndex = inc(takeIndex);        --count;        notFull.signal();        return x;    }

在第7行通知等待在notFull上的线程。

0 0
原创粉丝点击