ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

典型回答

有时候我们把并发包下面的所有容器都习惯叫作并发容器,但是严格来讲,只有以“Concurrent”为前缀的容器才是真正的并发容器。

  • ConcurrentLinkedQueue基于lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。
  • 而LinkedBlockingQueue内部则是基于锁,并提供了BlockingQueue的等待性方法。

不知道你有没有注意到,java.util.concurrent包提供的容器从命名上可以大概分为Concurrent、CopyOnWrite和Blocking三类,同样是线程安全容器,可以简单认为:

Concurrent类型容器没有CopyOnWrite之类容器相对较重的修改开销。但是,凡事都是有代价的,Concurrent往往提供了较低的遍历一致性。或称之为“弱一致性”,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。

与弱一致性对应的,就是其它同步容器常见的行为“fast-fail”,也就是检测到容器在遍历过程中发生了修改,则抛出ConcurrentModificationException,不再继续遍历。

弱一致性的另外一个体现是,size等操作准确性是有限的,未必是百分百准确。与此同时,读取的性能具有一定的不确定性。

知识扩展

1、线程安全队列一览

在本专栏第9讲中介绍过,常见的集合中如LinkedList是个Deque,即双端队列,只不过不是线程安全的。下面这张图是Java并发类库提供的各种线程安全队列实现。

从行为特征来看,绝大部分Queue都是实现了BlockingQueue接口。在常规队列操作基础上,Blocking意味着其提供了特定的等待性操作,获取(take)时等待元素进队,或者插入(put)时等待队列出现空位。

/**
 * 获取并移除队列头结点,如果必要,其会等待直到队列出现元素
 * ...
 */
E take() throws InterruptedException;
/**
 * 插入元素,如果队列已满,则等待直到队列出现空闲空间
 * ...
 */
void put(E e) throws InterruptedException;

另一个需要注意的点,就是是否有界。这一点也往往会影响我们在应用开发中的选择,这里简单总结一下。

  • ArrayBlockingQueue是最典型的有界队列,其内部以final的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建ArrayBlockingQueue时,都要指定容量。
  • LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为Integer.MAX_VALUE,几乎没有机会到达边界。
  • SynchronousQueue,这是一个非常奇葩的队列实现,每个获取操作都要等待插入操作,反之每个插入操作也都要等待获取动作。那么这个队列的容量是多少呢?是1吗?其实不是,其内部容量是0。
  • PriorityBlockingQueue是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受到系统资源影响。
  • DelayQueue和LinkedTransferQueue同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是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();

ArrayBlockingQueue的实现与之是有区别的,notEmpty和notFull都是同一个再入锁的条件变量。LinkedBlockingQueue则改进了锁操作的粒度,头、尾操作使用不同的锁,所以在通用场景下,它的吞吐量相对要更好一些。

LinkedBlockingQueue的take方法的实现也与ArrayBlockingQueue不同,是基于CAS的无锁技术,不需要在每个操作时使用锁,所以扩展性表现要更加优异。

2、队列使用场景与典型用例

在实际开发中,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> queue) {
      this.queue = queue;
    }
    @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> queue) {
      this.queue = queue;
    }
    @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的性能表现,往往大大超过其它实现,尤其是在队列元素较小的场景

猜你喜欢

转载自blog.csdn.net/qweqwruio/article/details/81359801