JDK并发容器初步认识

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上的线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值