JDK源码阅读(九):JUC之并发集合

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aimomo007/article/details/89340021


JUC包中的并发集合主要分为非阻塞队列和阻塞队列

非阻塞队列

非阻塞队列的特色就是队列里面没有数据时,操作队列出现异常或返回null,不具有等待/阻塞的特色。

免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取这就可以看到这个修改了。

ConcurrentHashMap

ConcurrentHashMap类是支持并发操作的Map对象。
与HashTable的区别:HashTable不支持在循环中remove()元素,HashTable在获得Iterator对象后,不允许更改其结构,否则出现java.util.ConcurrentModificationException异常。

实现原理

ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代HashTable。通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费。那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap的结构

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。ConcurrentHashMap的类图如下所示:

在这里插入图片描述

在这里插入图片描述

这是java8之前ConcurrentHashMap的实现原理,每个Segement其实就相当于一个小的Hashtable,在java8中,对ConcurrentHashMap的结构进行了更新,锁的粒度更加小

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

ConcurrentSkipListMap

类ConcurrentSkipListMap支持根据对象的compareTo()方法进行排序

ConcurrentSkipListSet

类ConcurrentSkipListSet支持排序而且不允许重复的元素

ConcurrentLinkedQueue

类ConcurrentLinkedQueue提供了并发环境的队列操作。

  • 方法poll()当没有获得数据时返回null,如果有数据时则移除表头,并将表头进行返回。
  • 方法element()当没有获得数据时出现NoSuchElementException异常,如果有数据时则返回表头项。
  • 方法peek()当没有获得数据时返回为null,如果有数据时则不移除表头,并将表头进行返回。

ConcurrentLinkedDeque

ConcurrentLinkedQueue仅支持对列头进行操作,而ConcurrentLinkedDeque支持对列头列尾双向操作。

CopyOnWriteArrayList

线程安全的ArrayList,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得赋值的数组在被修改时,读取操作可以安全的执行,当修改完成时,一个原子性的操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。

CopyOnWriteArraySet

线程安全的HashSet

阻塞队列

  • 如果阻塞队列是空的,从阻塞队列中取东西的操作将会被阻塞进入等待状态,直到阻塞队列中添加了元素才会被唤醒。
  • 如果阻塞队列是满的,从阻塞队列中存放元素的操作将会进行等待状态,知道阻塞队列里有剩余空间才会被唤醒继续操作。

ArrayBlockingQueue

类ArrayBlockingQueue提供一种有界阻塞队列的功能。底层使用数组实现。

LinkedBlockingQueue

类LinkedBlockingQueue和ArrayBlockingQueue在功能上大体一样,只不过ArrayBlockingQueue是有界的,而LinkedBlockingQueue是无界的,但也可以定义为有界的。底层使用链表实现。

从LinkedBlockingQueue的几个常用方法讲解它是如何实现阻塞的

offer()

//采用AtomicInteger保存队列中的元素个数
private final AtomicInteger count = new AtomicInteger();
//插入操作的重入锁
private final ReentrantLock putLock = new ReentrantLock();
//不满
private final Condition notFull = putLock.newCondition();

//向队列中插入一个元素
public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
    
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            if (count.get() < capacity) {
                enqueue(node);
                //通过CAS增加当前队列中的元素个数
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    //唤醒因为队列不满而阻塞的线程
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
    
        if (c == 0)
            //唤醒因为队列为空阻塞的线程
            signalNotEmpty();
        return c >= 0;
    }

根据源码得知,阻塞队列的安全主要是采用JUC包中的原子类和锁来完成的,会在后面的章节详细介绍

poll()

//取出操作的重入锁
private final ReentrantLock takeLock = new ReentrantLock();

//非空
private final Condition notEmpty = takeLock.newCondition();

//从队列中取出一个元素
public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)
        return null;
    E x = null;
    int c = -1;

    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        if (count.get() > 0) {
            x = dequeue();
            //通过CAS减少当前队列中的元素个数
            c = count.getAndDecrement();
            
            if (c > 1)
                //唤醒因为队列为空而阻塞的线程
                notEmpty.signal();
        }
    } finally {
        takeLock.unlock();
    }

    if (c == capacity)
        //唤醒因为队列不满而阻塞的线程
        signalNotFull();
    return x;
}

poll()方法主要是从

take()

//从队列中取出一个元素
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
    
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            //当队列为空时会一直阻塞
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    
        if (c == capacity)
            signalNotFull();
        return x;
    }

从源码中可以看出,poll()和take()的区别是当队列为空时是否会进行阻塞。

LinkedBlockingDeque

类LinkedBlockingQueue只支持对列头的操作,而LinkedBlockingDeque支持对双端结点的操作。

PriorityBlockingQueue

类PriorityBlockingQueue支持在并发情况下的优先级队列。

SynchronousQueue

类SynchronousQueue为异步队列,是一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作,反之亦然。

LinkedTransferQueue

类LinkedTransferQueue提供的功能与SynchronousQueue有些类似,但其具有嗅探功能,也就是可以尝试性地添加一些数据。

DelayQueue

类DelayQueue提供一种延时执行任务的队列。

猜你喜欢

转载自blog.csdn.net/aimomo007/article/details/89340021