juc并发集合类

集合总览

Queue 接口
----ConcurrentLinkedQueue 实现类
--------BlockingQueue 接口
------------ArrayBlockingQueue实现类
------------DelayQueue 实现类
------------PriorityBlockingQueue 实现类
------------SynchronousQueue实现类
--------Deque接口
------------ArrayDueue实现类
------------LinkedList实现类
------------BlockingDeque 接口
----------------LinkedBlockingDeque 实现类
CopyOnWriteArrayList实现类
CopyOnwriteArraySet实现类
ConcurrentSkipListSet实现类
ConcurrentMap接口
----ConcurrentHashMap实现类
----ConcurrentNavigableMap接口
--------ConcurrentSkipListMap实现类

juc并发集合类_第1张图片

ConcurrentHashMap

ConcurrentHashMap同HashMap一样也是基于散列表的map,但是它提供了一种与HashTable完全不同的加锁策略提供更高效的并发性和伸缩性。ConcurrentHashMap在JDK 1.7 和JDK 1.8中有一些区别。这里我们分开介绍一下。

  • JDK 1.7
    ConcurrentHashMap在JDK 1.7中,提供了一种粒度更细的加锁机制来实现在多线程下更高的性能,这种机制叫分段锁(Lock Striping)。提供的优点是:在并发环境下将实现更高的吞吐量,而在单线程环境下只损失非常小的性能。可以这样理解分段锁,就是将数据分段,对每一段数据分配一把锁。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()、isEmpty()、containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。如下图:
    juc并发集合类_第2张图片
    ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
  • JDK 1.8
    而在JDK 1.8中,ConcurrentHashMap主要做了两个优化:同HashMap一样,链表也会在长度达到8的时候转化为红黑树,这样可以提升大量冲突时候的查询效率;以某个位置的头结点(链表的头结点或红黑树的root结点)为锁,配合自旋+CAS避免不必要的锁开销,进一步提升并发性能。
  • ConcurrentNavigableMap接口与ConcurrentSkipListMap类
    ConcurrentNavigableMap接口继承了NavigableMap接口,这个接口提供了针对给定搜索目标返回最接近匹配项的导航方法。ConcurrentNavigableMap接口的主要实现类是ConcurrentSkipListMap类。从名字上来看,它的底层使用的是跳表(SkipList)的数据结构。跳表是一种”空间换时间“的数据结构,可以使用CAS来保证并发安全性。
  • 跳表(SkipList)
    我们看名字,跳表这个词可能有点是链表结构,是的没错,就是全部的链表结构,而且是有序的链表结构。但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但是由于在内存中的存储的不确定性,不能这做。

但是我们可以使用二分法的思想,在链表结构中选择瞄点,这个瞄点和原始点使用指针连接,查找的时候,先查瞄点,找到合适的瞄点后,通过指针映射到原始数据节点,再往后查找,思想和二分法查找一摸一样。通过描述可能还不能完全理解,可以看下面的图:

juc并发集合类_第3张图片
多级索引,空间换时间

CopyOnWritexxx

  • 什么是copyonwritexxx
    在说到CopyOnWrite容器之前我们先来谈谈什么是CopyOnWrite机制,CopyOnWrite是计算机设计领域中的一种优化策略,也是一种在并发场景下常用的设计思想——写入时复制思想。那什么是写入时复制思想呢?就是当有多个调用者同时去请求一个资源数据的时候,有一个调用者出于某些原因需要对当前的数据源进行修改,这个时候系统将会复制一个当前数据源的副本给调用者修改。CopyOnWrite容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。这样做的好处在于,我们可以在并发的场景下对容器进行"读操作"而不需要"加锁",从而达到读写分离的目的。从JDK 1.5 开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器 ,分别是CopyOnWriteArrayList和CopyOnWriteArraySet 。我们着重给大家介绍一下CopyOnWriteArrayList。
  • CopyOnWriteArrayList
    优点: CopyOnWriteArrayList经常被用于“读多写少”的并发场景,是因为CopyOnWriteArrayList无需任何同步措施,大大增强了读的性能。在Java中遍历线程非安全的List(如:ArrayList和 LinkedList)的时候,若中途有别的线程对List容器进行修改,那么会抛出ConcurrentModificationException异常。CopyOnWriteArrayList由于其"读写分离",遍历和修改操作分别作用在不同的List容器,所以在使用迭代器遍历的时候,则不会抛出异常。
    缺点: 第一个缺点是CopyOnWriteArrayList每次执行写操作都会将原容器进行拷贝了一份,数据量大的时候,内存会存在较大的压力,可能会引起频繁Full GC(ZGC因为没有使用Full GC)。比如这些对象占用的内存比较大200M左右,那么再写入100M数据进去,内存就会多占用300M。
    第二个缺点是CopyOnWriteArrayList由于实现的原因,写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据。只能保证最终一致性
    现在我们来看一下CopyOnWriteArrayList的add操作源码,它的逻辑很清晰,就是先把原容器进行copy,然后在新的副本上进行“写操作”,最后再切换引用,在此过程中是加了锁的.
public boolean add(E e) {

    // ReentrantLock加锁,保证线程安全
    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();
    }
}

Queue

JDK并没有提供线程安全的List类,因为对List来说,很难去开发一个通用并且没有并发瓶颈的线程安全的List。因为即使简单的读操作,拿contains() 这样一个操作来说,很难搜索的时候如何避免锁住整个list。所以退一步,JDK提供了对队列和双端队列的线程安全的类:ConcurrentLinkedDeque和ConcurrentLinkedQueue。因为队列相对于List来说,有更多的限制。这两个类是使用CAS来实现线程安全的。
ConcurrentLinkedQueue由head 和tail节点组成,doug lea 使用hops变量来控制并减少tail节点的更新频率,当tail节点和尾节点的距离大于等于常量hops时才更新tail,tail和尾节点的距离越长,使用cas更新尾节点的次数就越少。

BlockingQueue

juc并发集合类_第4张图片

实现类

  • ArrayBlockingQueue
    由数组结构组成的有界阻塞队列。内部结构是数组,故具有数组的特性。
public ArrayBlockingQueue(int capacity, boolean fair){
    //..省略代码
}

可以初始化队列大小, 且一旦初始化不能改变。构造方法中的fair表示控制对象的内部锁是否采用公平锁,默认是非公平锁。

  • LinkedBlockingQueue
    由链表结构组成的有界阻塞队列。内部结构是链表,具有链表的特性。默认队列的大小是Integer.MAX_VALUE,也可指定大小。此队列按照先进先出的原则对元素进行排序。

  • DelayQueue
    该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
    DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

  • PriorityBlockingQueue
    基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是公平锁。

  • SynchronousQueue
    这个队列比较特殊,没有任何内部容量,甚至连一个队列的容量都没有。并且每个 put 必须等待一个 take,反之亦然。需要区别容量为1的ArrayBlockingQueue、LinkedBlockingQueue。以下方法的返回值,可以帮助理解这个队列:
    iterator() 永远返回空,因为里面没有东西peek()
    永远返回null
    put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
    offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
    take() 取出并且remove掉queue里的element,取不到东西他会一直等。poll() 取出并且remove掉queue里的element,只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
    isEmpty() 永远返回true
    remove()&removeAll() 永远返回false

  • LinkedTransferQueue
    无界 阻塞队列,多了tryTransfer和transfer方法

  • LinkedBlockingDeque
    双向阻塞队列,可以用在工作窃取模式中。

// 生产者-消费者模式,借助
public class Test {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);

    public static void main(String[] args)  {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();

        producer.start();
        consumer.start();
    }

    class Consumer extends Thread{

        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while(true){
                try {
                    queue.take();
                    System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread{

        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while(true){
                try {
                    queue.put(1);
                    System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

原理

阻塞队列的原理很简单,利用了Lock锁的多条件(Condition)阻塞控制(通知模式)。接下来我们分析ArrayBlockingQueue JDK 1.8 的源码。首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同一个锁(lock)初始化了两个监视器,分别是notEmpty和notFull。这两个监视器的作用目前可以简单理解为标记分组,当该线程是put操作时,给他加上监视器notFull,标记这个线程是一个生产者;当线程是take操作时,给他加上监视器notEmpty,标记这个线程是消费者。

Set

JDK提供了ConcurrentSkipListSet,是线程安全的有序的集合。底层是使用ConcurrentSkipListMap实现

你可能感兴趣的:(juc)