持续创作,加快生长!这是我参加「日新方案 10 月更文应战」的第22天,点击检查活动概况

前言

今天介绍一下线程安全行列。Java 标准库供给了非常多的线程安全行列,很简略混杂。

本篇博文的重点是,并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么差异?

概述

有时候咱们把并发包下面的所有容器都习惯叫作并发容器,可是严厉来讲,相似 ConcurrentLinkedQueue 这种 “Concurrent..” 容器,才是真正代表并发。

关于问题中它们的差异:

  • Concurrent 类型根据 lock-free,在常见的多线程访问场景,一般能够供给较高吞吐量。
  • 而 LinkedBlockingQueue 内部则是根据锁,并供给了 BlockingQueue 的等候性办法。

不知道你有没有注意到,java.util.concurrent 包供给的容器(Queue、List、Set)、Map,从命名上能够大约区分为 Concurrent*、CopyOnWrite 和 Blocking 等三类,同样是线程安全容器,能够简略以为:

  • Concurrent 类型没有相似 CopyOnWrite 之类容器相对较重的修正开支。
  • 可是,凡事都是有代价的,Concurrent 往往供给了较低的遍历一致性。你能够这样了解所谓的弱一致性,例如,当运用迭代器遍历时,假如容器产生修正,迭代器仍然能够持续进行遍历。
  • 与弱一致性对应的,是同步容器常见的行为 “fail-fast”,也便是检测到容器在遍历过程中产生了修正,则抛出 ConcurrentModificationException,不再持续遍历。
  • 弱一致性的另外一个别现是,size 等操作精确性是有限的,未必是 100% 精确。
  • 与此同时,读取的功能具有必定的不确定性。

正文

线程安全行列

在 【JAVA】对比 Vector、ArrayList、LinkedList 有何差异? 中介绍过,常见的调集中如 LinkedList 是个 Deque,只不过不是线程安全的。下面这张图是 Java 并发类库供给的各式各样的线程安全行列完成,注意,图中并未将非线程安全部分包含进来。

【JAVA】并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么区别?

咱们能够从不同的视点进行分类,从根本的数据结构的视点剖析,有两个特别的 Deque 完成,ConcurrentLinkedDeque 和 LinkedBlockingDeque。Deque 的侧重点是支撑对行列头尾都进行刺进和删去,所以供给了特定的办法,如:

  • 尾部刺进时需求的 addLast(e)、offerLast(e)。
  • 尾部删去所需求的 removeLast()、pollLast()。

从上面这些视点,能够了解 ConcurrentLinkedDeque 和 LinkedBlockingQueue 的主要功能差异,也就满足日常开发的需求了。可是假如咱们深入一些,通常会愈加重视下面这些方面。

从行为特征来看,绝大部分 Queue 都是完成了 BlockingQueue 接口。在常规行列操作基础上,Blocking 意味着其供给了特定的等候性操作,获取时(take)等候元素进队,或许刺进时(put)等候行列呈现空位。

/**
 * 获取并移除行列头结点,假如必要,其会等候直到行列呈现元素
    …
 */
E take() throws InterruptedException;
/**
 * 刺进元素,假如行列已满,则等候直到行列呈现空闲空间
   …
 */
void put(E e) throws InterruptedException;  

另一个 BlockingQueue 经常被考察的点,便是是否有界(Bounded、Unbounded),这一点也往往会影响咱们在运用开发中的挑选,这儿简略总结一下。

  • ArrayBlockingQueue 是最典型的的有界行列,其内部以 final 的数组保存数据,数组的巨细就决定了行列的鸿沟,所以咱们在创立 ArrayBlockingQueue 时,都要指定容量,如
    public ArrayBlockingQueue(int capacity, boolean fair)
    
  • LinkedBlockingQueue,简略被误解为无鸿沟,但其实其行为和内部代码都是根据有界的逻辑完成的,只不过假如咱们没有在创立行列时就指定容量,那么其容量约束就自动被设置为 Integer.MAX_VALUE,成为了无界行列。
  • SynchronousQueue,这是一个非常奇葩的行列完成,每个删去操作都要等候刺进操作,反之每个刺进操作也都要等候删去动作。那么这个行列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。
  • PriorityBlockingQueue 是无鸿沟的优先行列,尽管严厉意义上来讲,其巨细总之是要受系统资源影响。
  • DelayedQueueLinkedTransferQueue 同样是无鸿沟的行列。对于无鸿沟的行列,有一个天然的结果,便是 put 操作永久也不会产生其他 BlockingQueue 的那种等候情况。

假如咱们剖析不同行列的底层完成,BlockingQueue 根本都是根据锁完成,一起来看看典型的 LinkedBlockingQueue。

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

在介绍 ReentrantLock 的条件变量用法的时候剖析过 ArrayBlockingQueue,不知道你有没有注意到,其条件变量与 LinkedBlockingQueue 版别的完成是有差异的。notEmpty、notFull 都是同一个再入锁的条件变量,而 LinkedBlockingQueue 则改进了锁操作的粒度,头、尾操作运用不同的锁,所以在通用场景下,它的吞吐量相对要更好一些。

下面的 take 办法与 ArrayBlockingQueue 中的完成,也是有不同的,由于其内部结构是链表,需求自己保护元素数量值,请参阅下面的代码。

public E take() throws InterruptedException {
    final E x;
    final int c;
    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;
}

相似 ConcurrentLinkedQueue 等,则是根据 CAS 的无锁技术,不需求在每个操作时运用锁,所以扩展性体现要愈加优异。

相对比较另类的 SynchronousQueue,在 Java 6 中,其完成产生了非常大的变化,运用 CAS 替换掉了原本根据锁的逻辑,同步开支比较小。它是 Executors.newCachedThreadPool() 的默认行列。

行列运用场景与典型用例

在实践开发中,Queue 被广泛运用在生产者 – 消费者场景,比方运用 BlockingQueue 来完成,由于其供给的等候机制,咱们能够少操心很多和谐作业,参阅下面样例代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ConsumerProducer {
    public static final String EXIT_MSG  = "Good bye!";
    public static void main(String[] args) {
        // 运用较小的行列,以更好地在输出中展现其影响
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
    static class Producer implements Runnable {
        private BlockingQueue<String> queue;
        public Producer(BlockingQueue<String> q) {
            this.queue = q;
        }
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                try{
                    Thread.sleep(5L);
                    String msg = "Message" + i;
                    System.out.println("Produced new item: " + msg);
                    queue.put(msg);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                System.out.println("Time to say good bye!");
                queue.put(EXIT_MSG);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    static class Consumer implements Runnable{
        private BlockingQueue<String> queue;
        public Consumer(BlockingQueue<String> q){
            this.queue=q;
        }
        @Override
        public void run() {
            try{
                String msg;
                while(!EXIT_MSG.equalsIgnoreCase( (msg = queue.take()))){
                    System.out.println("Consumed item: " + msg);
                    Thread.sleep(10L);
                }
                System.out.println("Got exit message, bye!");
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上面是一个典型的生产者 – 消费者样例,假如运用非 Blocking 的行列,那么咱们就要自己去完成轮询、条件判断(如检查 poll 返回值是否 null)等逻辑,假如没有特别的场景要求,Blocking 完成起来代码愈加简略、直观。

前面介绍了各种行列完成,在日常的运用开发中,怎么进行挑选呢?

以 LinkedBlockingQueue、ArrayBlockingQueue 和 SynchronousQueue 为例,咱们一起来剖析一下,根据需求能够从很多方面考量:

  • 考虑运用场景中对行列鸿沟的要求。ArrayBlockingQueue 是有清晰的容量约束的,而 LinkedBlockingQueue 则取决于咱们是否在创立时指定,SynchronousQueue 则干脆不能缓存任何元素。
  • 从空间运用视点,数组结构的 ArrayBlockingQueue 要比 LinkedBlockingQueue 紧凑,由于其不需求创立所谓节点,可是其初始分配阶段就需求一段连续的空间,所以初始内存需求更大。
  • 通用场景中,LinkedBlockingQueue 的吞吐量一般优于 ArrayBlockingQueue,由于它完成了愈加细粒度的锁操作。
  • ArrayBlockingQueue 完成比较简略,功能更好猜测,归于体现安稳的 “选手”。
  • 假如咱们需求完成的是两个线程之间接力性(handoff)的场景,你可能会挑选 CountDownLatch,可是SynchronousQueue 也是完美契合这种场景的,并且线程间和谐和数据传输一致起来,代码愈加规范。
  • 可能令人意外的是,很多时候 SynchronousQueue 的功能体现,往往大大超过其他完成,尤其是在行列元素较小的场景。

跋文

以上便是【JAVA】并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么差异?的所有内容了;

剖析了 Java 中让人眼花缭乱的各种线程安全行列,试图从几个视点,让每个行列的特点愈加清晰,从而希望削减你在日常作业中运用时的困扰。

上篇精讲:【JAVA】一个线程两次调用 start() 办法会呈现什么情况?

我是,等待你的重视;

创作不易,请多多支撑;

系列专栏:面试精讲 JAVA