java核心技术36讲笔记 第20讲 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?

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

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

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

  • Concurrent类型没有类似CopyOnWrite之类容器相对较重的修改开销。
  • 但是,凡事都是有代价的, Concurrent往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续
    进行遍历。
  • 与弱一致性对应的,就是我介绍过的同步容器常见的行为“fast-fail”,也就是检测到容器在遍历过程中发生了修改,则抛出ConcurrentModifcationException,不再继续遍历。
  • 弱一致性的另外一个体现是, size等操作准确性是有限的,未必是100%准确。
  • 与此同时,读取的性能具有一定的不确定性。

Lock free相关概念

Non-blocking algorithm/

无锁编程(Lockless Programming)是一系列技术,用于在不使用锁的情况下安全地操作共享数据。有无锁算法(lockless algorithms)可用于传递消息,共享列表和数据队列以及其他任务。

进行无锁编程时,您必须面对两个挑战:非原子操作和重新排序。

Xbox 360和Microsoft Windows的无锁编程注意事项

无锁编程介绍译文

原文

非线程队列一览

在这里插入图片描述

Deque的侧重点是支持对队列
头尾都进行插入和删除,所以提供了特定的方法,如:

  • 尾部插入时需要的addLast(e)、 oferLast(e)。
  • 尾部删除所需要的removeLast()、 pollLast()。

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

哪些队列是有界的,哪些是无界的?

常见的有界队列

  • ArrayBlockingQueue其内部以fnal的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建ArrayBlockingQueue时,都要指定容量,如
public ArrayBlockingQueue(int capacity, boolean fair)
  • LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被
    设置为Integer.MAX_VALUE,成为了无界队列。
  • SynchronousQueue,这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是1吗?其实不
    是的,其内部容量是0。

常见的无界队列

  • PriorityBlockingQueue 具有优先级的阻塞队列,严格意义上来讲,其大小总归是要受系统资源影响。

  • DelayedQueue 延时队列,使用场景

    • 缓存:清掉缓存中超时的缓存数据
    • 任务超时处理
    • 内部实现其实是采用带时间的优先队列,可重入锁,优化阻塞通知的线程元素leader
  • LinkedTransferQueue 简单的说也是进行线程间数据交换的利器,在SynchronousQueue 中就有所体现,并且并发大神 Doug Lea 对其进行了极致的优化,使用15个对象填充,加上本身4字节,总共64字节就可以避免缓存行中的伪共享问题,其实现细节较为复杂,可以说一下大致过程:

    比如消费者线程从一个队列中取元素,发现队列为空,他就生成一个空元素放入队列 , 所谓空元素就是数据项字段为空。然后消费者线程在这个字段上旅转等待。这叫保留。直到一个生产者线程意欲向队例中放入一个元素,这里他发现最前面的元素的数据项字段为 NULL,他就直接把自已数据填充到这个元素中,即完成了元素的传送。大体是这个意思,这种方式优美了完成了线程之间的高效协作。
    LinkedTransferQueue

无界队列的共同点 :
  • put 操作永远都不会阻塞,空间限制来源于系统资源的限制

Queue底层实现

BlockingQueue基本都是基于锁实现,一起来看看典型的LinkedBlockingQueue

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

ReentrantLock条件变量与LinkedBlockingQueue版本的实现是有区别

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

take方法与ArrayBlockingQueue中的实现,也是有不同的,由于其内部结构是链表,需要自己维护元素数量值。

SynchronousQueue,在Java 6中,其实现发生了非常大的变化,利用CAS替换掉了原本基于锁的逻辑,同步开销比较小。它是Executors.newCachedThreadPool()的默认队列。

队列使用场景与典型用例

BlockingQueue生产者-消费者场景

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ConsumerProducer {
public satic fnal String EXIT_MSG = "Good bye!";
public satic void main(String[] args) {
// 使用较小的队列,以更好地在输出中展示其影响
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).sart();
new Thread(consumer).sart();
}
satic 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;
Sysem.out.println("Produced new item: " + msg);
queue.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Sysem.out.println("Time to say good bye!");
queue.put(EXIT_MSG);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
satic 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()))){
Sysem.out.println("Consumed item: " + msg);
Thread.sleep(10L);
}
Sysem.out.println("Got exit message, bye!");
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}

LinkedBlockingQueue、 ArrayBlockingQueue和SynchronousQueue如何进行选择

  • 考虑应用场景中对队列边界的要求。 ArrayBlockingQueue是有明确的容量限制的,而LinkedBlockingQueue则取决于我们是否在创建时指定, SynchronousQueue则干脆不
    能缓存任何元素。
  • 从空间利用角度,数组结构的ArrayBlockingQueue要比LinkedBlockingQueue紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存
    需求更大。
  • 通用场景中, LinkedBlockingQueue的吞吐量一般优于ArrayBlockingQueue,因为它实现了更加细粒度的锁操作。
  • ArrayBlockingQueue实现比较简单,性能更好预测,属于表现稳定的“选手”。
  • 如果我们需要实现的是两个线程之间接力性(handof)的场景,按照专栏上一讲的例子,你可能会选择CountDownLatch,但是SynchronousQueue也是完美符合这种场景
    的,而且线程间协调和数据传输统一起来,代码更加规范。
  • 可能令人意外的是,很多时候SynchronousQueue的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。

wait/notify模拟阻塞队列

wait/notify模拟阻塞队列

发布了93 篇原创文章 · 获赞 26 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/sxj159753/article/details/101004919