并发系列——JUC高级篇(六)并发队列

java里的阻塞队列

  JDK提供了7种阻塞队列。如下:

1)ArrayBlockingQueue:由数组结构组成的有界阻塞队列。

2)LinkedBlockingQueue:由链表结构组成的有界阻塞队列。

3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列

4)DelayQueue:使用优先级队列实现的无界阻塞队列

5)SynchronousQueue:不存储元素的阻塞队列

6)LinkedTransferQueue:由链表结构组成的无界阻塞队列

7)LinkedBlockingQueue:由链表结构组成的双向阻塞队列


 7种阻塞队列介绍

ArrayBlockingQueue

  ArrayBlockingQueue是一个用数组实现的有界阻塞队列,在初始化构造的时候需要指定队列的容量。此队列按照先进先出(FIFO)的原则对元素进行排序。

  具有如下特点:

1. 队列的容量一旦在构造时指定,后续不能改变;

2. 插入元素时,在队尾进行;删除元素时,在队首进行;

3. 队列满时,调用特定方法插入元素会阻塞线程;队列空时,删除元素也会阻塞队列;

4. 支持公平/非公平策略,默认为非公平策略。

  这里的公平策略,是指当线程从阻塞到唤醒后,以最初请求的顺序(FIFO)来添加或删除元素;非公平策略指线程被唤醒后,谁先抢占到锁,谁就能往队列中添加/删除,顺序是随机的。

  ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。

  在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。


LinkedBlockingQueue

  LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度是Integer.MAX_VALUE。此队列按照先进先出(FIFO)的原则对元素进行排序。

  LinkedBlockingQueue除了底层数据结构是单链表与ArrayBlockingQueue不同外,另外一个特点就是:它维护了两把锁——takeLockputLock。

  takeLock用于控制出队的并发,putLock用于控制入队的并发。

  这也就意味着,同一时刻,只能只有一个线程能执行入队/出队操作,其余入队/出队线程会被阻塞; 但是,入队和出队之间可以并发执行,即同一时刻,可以同时有一个线程进行入队,另一个线程进行出队,这样就可以提升吞吐量。

    LinkedBlockingQueue和ArrayBlockingQueue比较主要有以下区别:

  1. 队列大小不同。ArrayBlockingQueue初始构造时必须指定大小,而LinkedBlockingQueue构造时既可以指定大小,也可以不指定(默认为Integer.MAX_VALUE,近似于无界);
  2. 底层数据结构不同。ArrayBlockingQueue底层采用数组作为数据存储容器,而LinkedBlockingQueue底层采用单链表作为数据存储容器;
  3. 两者的加锁机制不同。ArrayBlockingQueue使用一把全局锁,即入队和出队使用同一个ReentrantLock锁;而LinkedBlockingQueue进行了锁分离,入队使用一个ReentrantLock锁(putLock),出队使用另一个ReentrantLock锁(takeLock);
  4. LinkedBlockingQueue不能指定公平/非公平策略(默认都是非公平),而ArrayBlockingQueue可以指定策略。

 

PriorityBlockingQueue

  PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排列。在构造的时候可以指定队列的初始容量。

1. PriorityBlockingQueue是一种优先级队列,也就是元素并不是以FIFO的方式出/入队,而是按照权重大小的顺序出队;

2. PriorityBlockingQueue是真正的无界队列(仅受内存大小限制),它不像ArrayBlockingQueue那样构造时必须指定最大容量,也不像LinkedBlockingQueue默认最大容量为Integer.MAX_VALUE;

3. 由于PriorityBlockingQueue是按照元素的权重进入排序,所以队列中的元素必须是可以比较的,也就是说元素必须实现Comparable接口;

4. 由于PriorityBlockingQueue无界队列,所以插入元素永远不会阻塞线程;

5. PriorityBlockingQueue底层是一种基于数组实现的堆结构

  注意:堆分为“大顶堆”和“小顶堆”,PriorityBlockingQueue会依据元素的比较方式选择构建大顶堆或小顶堆。比如:如果元素是Integer这种引用类型,那么默认就是“小顶堆”,也就是每次出队都会是当前队列最小的元素。

  PriorityBlockingQueue属于比较特殊的阻塞队列,适用于有元素优先级要求的场景。它的内部和ArrayBlockingQueue一样,使用一个了全局独占锁来控制同时只有一个线程可以进行入队和出队,另外由于该队列是无界队列,所以入队线程并不会阻塞。

  PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部通过使用堆(数组形式)来维护元素顺序,它的内部数组是可扩容的,扩容和出/入队可以并发进行。


 

SynchronousQueue

  SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

  它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。通过构造入参创建公平性访问的SynchronousQueue,如果设为true,则等待的线程会采用先进先出的顺序访问队列。

  SynchronousQueue的底层实现包含两种数据结构——栈和队列。这是一种非常特殊的阻塞队列。

  特点简要概述如下:

1. 入队线程和出队线程必须一一匹配,否则任意先到达的线程会阻塞。比如ThreadA进行入队操作,在有其它线程执行出队操作之前,ThreadA会一直等待,反之亦然;

2. SynchronousQueue内部不保存任何元素,也就是说它的容量为0,数据直接在配对的生产者和消费者线程之间传递,不会将数据缓存到队列中。

3. SynchronousQueue支持公平/非公平策略。其中非公平模式,基于内部数据结构——“”来实现公平模式,基于内部数据结构——“队列”来实现


 

DelayQueue

  DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

  DelayQueue非常有用,可以将DelayQueue运用在以下应用场景

1) 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

2) 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间。

  它的特点:

 

1. DelayQueue是无界阻塞队列;

 

2. 队列中的元素必须实现Delayed接口,元素过期后才会从队列中取走;


LinkedBlockingDeque

  LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。

  和ConcurrentLinkedDeque类似,都是一种双端队列的结构,只不过LinkedBlockingDeque同时也是一种阻塞队列。

  LinkedBlockingDeque底层利用ReentrantLock实现同步,并不像ConcurrentLinkedDeque那样采用无锁算法

  LinkedBlockingDeque作为一种阻塞双端队列,提供了队尾删除元素和队首插入元素的阻塞方法。

  该类在构造时一般需要指定容量,如果不指定,则最大容量为Integer.MAX_VALUE。另外,由于内部通过ReentrantLock来保证线程安全,所以LinkedBlockingDeque的整体实现时比较简单的。

  另外,双端队列相比普通队列,主要是多了【队尾出队元素】/【队首入队元素】的功能。
  阻塞队列我们知道一般用于“生产者-消费者”模式,而双端阻塞队列在“生产者-消费者”就可以利用“双端”的特性,从队尾出队元素。


LinkedTransferQueue

  LinkedTransferQueue一种比较特殊的阻塞队列。

  我们知道,在普通阻塞队列中,当队列为空时,消费者线程(调用take或poll方法的线程)一般会阻塞等待生产者线程往队列中存入元素。而LinkedTransferQueue的transfer方法则比较特殊:

  1. 当有消费者线程阻塞等待时,调用transfer方法的生产者线程不会将元素存入队列,而是直接将元素传递给消费者;
  2. 如果调用transfer方法的生产者线程发现没有正在等待的消费者线程,则会将元素入队,然后会阻塞等待,直到有一个消费者线程来获取该元素。

  LinkedTransferQueue的特点简要概括如下:

  1. LinkedTransferQueue是一种无界阻塞队列,底层基于单链表实现;
  2. LinkedTransferQueue中的结点有两种类型:数据结点、请求结点;
  3. LinkedTransferQueue基于无锁算法实现。

  LinkedTransferQueue兼具了SynchronousQueue的特性以及无锁算法的性能,并且是一种无界队列:

和SynchronousQueue相比,LinkedTransferQueue可以存储实际的数据;

和其它阻塞队列相比,LinkedTransferQueue直接用无锁算法实现,性能有所提升。

  LinkedTransferQueue包含了ConcurrentLinkedQueue、SynchronousQueue、LinkedBlockingQueues三种队列的功能

 

下面举一个阻塞队列进行分析:

PriorityBlockingQueue

PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或最低的元素。内部使用二叉堆实现。

类图结构

 

PriorityBlockingQueue内部有一个数组queue,用来存放队列元素。allocationSpinLock是个自旋锁,通过CAS操作来保证同时只有一个线程可以扩容队列,状态为0或1。

由于这是一个优先队列,所以有一个comparator用来比较元素大小。

下面为构造函数:

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
}

可知默认队列容量为11,默认比较器为null,也就是使用元素的compareTo方法进行比较来确定元素的优先级,这意味着队列元素必须实现Comparable接口。

原理讲解

boolean offer()

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    // 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    // 扩容
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            // 通过对二叉堆的上浮操作保证最大或最小的元素总在根节点
            siftUpComparable(n, e, array);
        else
            // 使用了自定义比较器
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        // 激活因调用take()方法被阻塞的线程
        notEmpty.signal();
    } finally {
        // 释放锁
        lock.unlock();
    }
    return true;
}

流程比较简单,下面主要看扩容和建堆操作。

先看扩容。

private void tryGrow(Object[] array, int oldCap) {
    // 由前面的代码可知,调用tryGrow函数前先获取了独占锁,
    // 由于扩容比较费时,此处先释放锁,
    // 让其他线程可以继续操作(如果满足可操作的条件的话),
    // 以提升并发性能
    lock.unlock();
    Object[] newArray = null;
    // 通过allocationSpinLock保证同时最多只有一个线程进行扩容操作。
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,0, 1)) {
        try {
            // 当容量比较小时,一次只增加2容量
            // 比较大时增加一倍
            int newCap = oldCap + ((oldCap < 64) ?(oldCap + 2) : (oldCap >> 1));
            // 溢出检测
            if (newCap - MAX_ARRAY_SIZE > 0) {
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            // 释放锁,没用CAS是因为同时最多有一个线程操作allocationSpinLock
            allocationSpinLock = 0;
        }
    }
    // 如果当前线程发现有其他线程正在对队列进行扩容,
    // 则调用yield方法尝试让出CPU资源促使扩容操作尽快完成
    if (newArray == null)
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

下面来看建堆算法

private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        // 获取父节点,设子节点索引为k,
        // 则由二叉堆的性质可知,父节点的索引总为(k - 1) >>> 1
        int parent = (k - 1) >>> 1;
        // 获取父节点对应的值
        Object e = array[parent];
        // 只有子节点的值小于父节点的值时才上浮
        if (key.compareTo((T) e) >= 0)
            break;
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

如果了解二叉堆的话,此处代码是十分容易理解的。关于二叉堆,可参看《数据结构之二叉堆》

E poll()

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 出队
        return dequeue();
    } finally {
        lock.unlock();
    }
}

private E dequeue() {
    int n = size - 1;
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
        E result = (E) array[0];
        // 获取尾节点,在实现对二叉堆的下沉操作时要用到
        E x = (E) array[n];
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            // 下沉操作,保证取走最小的节点(根节点)后,新的根节点仍时最小的,二叉堆的性质依然满足
            siftDownComparable(0, x, array, n);
        else
            // 使用自定义比较器
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}

poll方法通过调用dequeue方法使最大或最小的节点出队并将其返回。

下面来看二叉堆的下沉操作。

private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;
        while (k < half) {
            // child为两个子节点(如果有的话)中较小的那个对应的索引
            int child = (k << 1) + 1;
            Object c = array[child];
            int right = child + 1;
            // 通过比较保证child对应的为较小值的索引
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            if (key.compareTo((T) c) <= 0)
                break;
            // 下沉,将较小的子节点换到父节点位置
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}

同上,对下沉操作有疑问的话可参考上述文章。

void put(E e)

调用了offer

public void put(E e){
    offer(e);
}

E take()

take操作的作用是获取二叉堆的根节点元素,如果队列为空则阻塞。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 阻塞可被中断
    lock.lockInterruptibly();
    E result;
    try {
        // 队列为空就将当前线程放入notEmpty条件队列
        // 使用while循环判断是为了避免虚假唤醒
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

DelayQueue

DelayQueue并发队列是一个无界阻塞延迟队列,队列中的每一个元素都有一个过期时间,当从队列中获取元素时只有过期元素才会出列。队列头元素是最快要过期的元素。

类图结构

 

DelayQueue内部使用PriorityQueue存放数据,使用ReentrantLock实现线程同步。 队列里的元素要实现Delayed接口(Delayed接口继承了Comparable接口),用以得到过期时间并进行过期时间的比较。

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

available是由lock生成的条件变量,用以实现线程间的同步。

leader是leader-follower模式的变体,用于减少不必要的线程等待。当一个线程调用队列的take方法变为leader线程后,它会调用条件变量available.waitNanos(delay)等待delay时间,但是其他线程(follower)则会调用available.await()进行无限等待。leader线程延迟时间过期后,会退出take方法,并通过调用available.signal()方法唤醒一个follower线程,被唤醒的线程会被选举为新的leader线程。

原理讲解

boolean offer(E e)

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 添加新元素
        q.offer(e);
        // 查看新添加的元素是否为最先过期的
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

上述代码首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加元素后,调用q.peek()方法返回的并不一定是当前添加的元素。当如果q.peek() == e,说明当前元素是最先要过期的,那么重置leader线程为null并激活available条件队列里的一个线程,告诉它队列里面有元素了。

E take()

获取并移除队列里面过期的元素,如果队列里面没有过期元素则等待。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 可中断
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek();
            // 为空则等待
            if (first == null)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                // 过期则成功获取
                if (delay <= 0)
                    return q.poll();
                // 执行到此处,说明头元素未过期    
                first = null; // don't retain ref while waiting
                // follower无限等待,直到被唤醒
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // leader等待lelay时间,则头元素必定已经过期
                        available.awaitNanos(delay);
                    } finally {
                        // 重置leader,给follower称为leader的机会
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            // 唤醒一个follower线程
            available.signal();
        lock.unlock();
    }
}

一个线程调用take方法时,会首先查看头元素是否为空,为空则直接等待,否则判断是否过期。 若头元素已经过期,则直接通过poll获取并移除,否则判断是否有leader线程。 若有leader线程则一直等待,否则自己成为leader并等待头元素过期。

E poll()

获取并移除头过期元素,如果没有过期元素则返回null。

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();
        // 若队列为空或没有元素过期则直接返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll();
    } finally {
        lock.unlock();
    }
}

int size()

计算队列元素个数,包含过期的和未过期的。

public int size() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return q.size();
    } finally {
        lock.unlock();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值