持续创作,加快生长!这是我参加「日新方案 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 并发类库供给的各式各样的线程安全行列完成,注意,图中并未将非线程安全部分包含进来。
咱们能够从不同的视点进行分类,从根本的数据结构的视点剖析,有两个特别的 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
是无鸿沟的优先行列,尽管严厉意义上来讲,其巨细总之是要受系统资源影响。 -
DelayedQueue
和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();
在介绍 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