1.Condition 接口与synchronized的比较
任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、 wait(long timeout)、notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。通过对比 Object 的监视器方法和 Condition 接口,可以更详细地了解 Condition 的特性,对比项与结果如表 1-1所示。
对比项 | Object Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock()获取锁 调用Lock.newCondition() 获取Condition对象 |
调用方式 | 直接调用 如:object.wait() | 直接调用 如:condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
唤醒在等待队列中的的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
2.Condition 接口与示例
Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。Condition 对象是由 Lock 对象(调用 Lock 对象的newCondition()方法)创建出来的,换句话说,Condition 是依赖 Lock 对象的。Condition 的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如代码清单 1-1 所示。
public class CondictionTest {
private Lock lock = new ReentrantLock();
// 初始化等待队列
private Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal(){
lock.lock();
try {
condition.signal();
} finally {
lock.unlock();
}
}
}
如示例所示,一般都会将 Condition 对象作为成员变量。当调用 await()方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal()方法,通知当前线程后,当前线程才从 await()方法返回,并且在返回前已经获取了锁。
Condition 定义的(部分)方法以及描述如表 1-2 所示。
方法名称 | 描述 |
---|---|
void await() throws InterruptedException | 当前线程进入等待状态直到被通知(signal)或中断。 |
void awaitUninterruptibly() | 当前线程进入等待状态直到被通知,该方法对中断不敏感。 |
long awaitNanos(long nanosTimeout) throws InterruptedException | 当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余的时间。 |
boolean await(long time, TimeUnit unit) throws InterruptedException | 当前线程进入等待状态直到被通知、中断或者超时。 |
boolean awaitUntil(Date deadline) throws InterruptedException | 当前线程进入等待状态直到被通知、中断或者到某个时间点。如何没有到指定时间被通知,方法返回true,否则,表示到了指定时间,返回false。 |
void signal() | 唤醒一个等待在Condition上的线程,该线程从等待方法返回必须前必须获得与Condition相关联的锁。 |
void signalAll() | 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。 |
3.使用Condition编写简单的有界队列
获取一个 Condition 必须通过 Lock 的 newCondition()方法。下面通过一个有界队列的示例来深入了解 Condition 的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现“空位”,如代码清单 1-2 所示。
public class SimpleBoundedQueue<T> {
// 队列
private Object[] items;
// 添加的下标
private int addIndex;
// 删除的下标
private int removeIndex;
// 数组的当前数量
private int count;
// 声明锁
private Lock lock = new ReentrantLock();
// 声明等待队列
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public SimpleBoundedQueue(int size){
items = new Object[size];
}
// 添加一个元素,如果数组满,则添加线程进入等待队列,直到有空位
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length){
// notFull 队列满了,进入等待队列
notFull.await();
}
items[addIndex] = t;
if (++addIndex == items.length){
addIndex = 0;
}
++count;
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object x = items[removeIndex];
if (++removeIndex == items.length) {
removeIndex = 0;
}
--count;
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
上述示例中,BoundedQueue 通过 add(T t)方法添加一个元素,通过 remove()方法移出一个元素。以添加方法为例。首先需要获得锁,目的是确保数组修改的可见性和排他性。当数组数量等于数组长度时,表示数组已满,则调用 notFull.await(),当前线程随之释放锁并进入等待状态。如果数组数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知等待在notEmpty 上的线程,数组中已经有新元素可以获取。在添加和删除方法中使用 while 循环而非 if 判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。
4.Condiction的实现分析
ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部类,因为 Condition 的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个 Condition 对象都包含着一个队列(以下称为等待队列),该队列是 Condition 对象实现等待/通知功能的关键。
下面将分析 Condition 的实现,主要包括:等待队列、等待和通知,下面提到的Condition 如果不加说明均指的是 ConditionObject。
4.1 等待队列
等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在 Condition 对象上等待的线程,如果一个线程调用了 Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类 AbstractQueuedSynchronizer.Node。
一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用 Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图 1-1 所示。
等待队列的基本结构如图所示,Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图1-2所示。
如图所示,Condition 的实现是同步器的内部类,因此每个 Condition 实例都能够访问同步器提供的方法,相当于每个 Condition 都拥有所属同步器的引用。
4.2 等待
调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁。如果从队列(同步队列和等待队列)的角度看 await()方法,当调用 await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。
Condition 的 await()方法,如代码清单 1-3 所示。
public final void await() throws InterruptedException {
if (Thread.interrupted())
// 如果线程中断标志位位true,抛出中断异常
throw new InterruptedException();
// 将当前线程加入等待队列,addConditionWaiter方法代码在下面
Node node = addConditionWaiter();
// 释放同步状态,也就是释放锁,fullyRelease(node)方法代码在下面
int savedState = fullyRelease(node);
// 初始化一个整型变量 interruptMode 为 0,用于记录等待过程中的中断模式。
int interruptMode = 0;
// 循环判断节点是否在同步队列中
while (!isOnSyncQueue(node)) {
// 如果不在,使用LockSupport.park(this) 将当前线程挂起,等待被唤醒。
LockSupport.park(this);
// 使用 checkInterruptWhileWaiting(node) 方法检查等待过程中是否发生了中断事件,并将返回的中断模式赋给 interruptMode
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 调用acquireQueued(node, savedState) 继续竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter
private Node addConditionWaiter() {
// 将t指针指向lastWaiter 尾节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
// 如果当前尾节点不为null,并且尾节点的状态不是CONDITION(表示该节点不是等待条件的节点)则清除队列中被取消的等待节点。
unlinkCancelledWaiters();
// 再次将 lastWaiter 的值赋给 t
t = lastWaiter;
}
// 将当前线程已Node.CONDITION 等待状态构造了一个新的节点node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 下面的逻辑就是将新节点加入到等待队列尾部
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#fullyRelease
final int fullyRelease(Node node) {
// failed 标志判断锁是否成功释放
boolean failed = true;
try {
// 获得锁的同步状态
int savedState = getState();
// 尝试释放锁
if (release(savedState)) {
// 释放成功
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用 Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException。
如果从队列的角度去看,当前线程加入 Condition 的等待队列,该过程如图 1-3示。如图所示,同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()方法把当前线程构造成一个新的节点并将其加入等待队列中。
4.3 通知
调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition 的 signal()方法,如代码清单 1-4 所示
public final void signal() {
// 判断当前线程是否获取了同步状态
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 将 first 指针指向头节点firstWaiter
Node first = firstWaiter;
if (first != null)
// 进行唤醒操作 doSignald(first)代码在下面
doSignal(first);
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#doSignal
private void doSignal(Node first) {
// 代码进入一个循环,一直执行下面的步骤,直到成功将节点传递给 transferForSignal(Node node) 方法或者队列中无其他等待节点为止。
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer#transferForSignal
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
// 检查节点的 waitStatus 是否可以改变。如果无法改变,说明节点已被取消,直接返回 false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 唤醒失败
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
// enq方法可以将节点安全的移动到同步队列尾部,并将p指针指向node的前驱节点
Node p = enq(node);
// 获得node节点的前驱节点的等待状态
int ws = p.waitStatus;
// 如果ws>0 说明前驱节点已被取消,或者尝试将前驱节点的等待状态更新为-1失败
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 唤醒node节点线程
LockSupport.unpark(node.thread);
// 表示唤醒成功
return true;
}
调用该方法的前置条件是当前线程必须获取了锁,可以看到 signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。节点从等待队列移动到同步队列的过程如图 1-4 所示。
通过调用同步器的 enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用 LockSupport 唤醒该节点的线程。
被唤醒后的线程,将从 await()方法中的 while 循环中退出(isOnSyncQueue(Node node)方法返回 true,节点已经在同步队列中),进而调用同步器的 acquireQueued()方法加入到获取同步状态的竞争中。
成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的 await()方法返回,此时该线程已经成功地获取了锁。
Condition 的 signalAll()方法,相当于对等待队列中的每个节点均执行一次 signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。
5.阻塞队列
5.1 什么是阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
1.支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2.支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
在阻塞队列不可用时,这两个附加操作提供了 4 种处理方式,如表 1-4 所示。
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
- 抛出异常:当队列满时,如果再往队列里插入元素,会抛出 IllegalStateException(“Queue full”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException 异常。
- 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回 true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回 null。
- 一直阻塞:当阻塞队列满时,如果生产者线程往队列里 put 元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里 take 元素,队列会阻塞住消费者线程,直到队列不为空。
- 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
如果是无界阻塞队列,队列不可能会出现满的情况,所以使用 put 或 offer方法永远不会被阻塞,而且使用 offer 方法时,该方法永远返回 true。
借用ArrayBlockingQueue 演示上面的方法,见代码清单1-5
public class BlockingQueueUseCase {
// 声明有界队列 ArrayBlockingQueue
private static ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue(1);
public static void main(String[] args) throws InterruptedException {
addTest();
removeTest();
checkTest();
}
//-----------------插入方法演示-------------------------
private static void addTest() throws InterruptedException {
// 先添加一个元素,让阻塞队列添加满
arrayBlockingQueue.add(1);
// 使用add方法继续添加元素,会抛出Exception in thread "main" java.lang.IllegalStateException: Queue full 的异常
arrayBlockingQueue.add(2);
// 使用offer(e) 往队列中添加元素, 返回false
boolean offer = arrayBlockingQueue.offer(2);
// 使用put(e) 往队列中添加元素 当前线程会进入阻塞
arrayBlockingQueue.put(2);
// 使用offer(e, timeout, unit) 方法往队列中添加元素, 10s后返回false
boolean offerFlag = arrayBlockingQueue.offer(2, 10, TimeUnit.SECONDS);
arrayBlockingQueue.clear();
}
//-----------------移除方法演示-------------------------
private static void removeTest() throws InterruptedException {
// 使用remove()方法会抛出异常Exception in thread "main" java.util.NoSuchElementException
arrayBlockingQueue.remove();
// 使用poll()方法移除元素 返回null
Integer ele = arrayBlockingQueue.poll();
// 使用take()移除元素会一直阻塞
arrayBlockingQueue.take();
// 使用poll(time, unit)方法移除元素 超时时间过后返回null
Integer element = arrayBlockingQueue.poll(10, TimeUnit.SECONDS);
}
//-----------------检查方法演示-------------------------
private static void checkTest() {
// 使用element()检查方法,抛出异常Exception in thread "main" java.util.NoSuchElementException
Integer element = arrayBlockingQueue.element();
// 使用peek()检查方法,返回null
Integer peek = arrayBlockingQueue.peek();
}
}
5.2 Java里的阻塞队列
JDK 7 提供了 7 个阻塞队列,如下。
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的阻塞队列(默认情况下无界,使用有参构造指定容量可构建有界阻塞队列)。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
5.2.1 ArrayBlockingQueue
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。
默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列。为了保证公平性,通常会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列。
ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue(1,true);
//java.util.concurrent.ArrayBlockingQueue#ArrayBlockingQueue(int, boolean)
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
此处的公平性只是针对获取到lock锁的公平性,并不是说这个公平性是保证哪个线程先调用获取获取队列的方法,举个例子:
我现在定义了一个阻塞队列,并且将阻塞队列声明为公平模式
// 声明了公平锁模式
ArrayBlockingQueue<Integer> arrayBlockingQueue = new ArrayBlockingQueue<>(1,true);
假如我现在有3个线程,分别为ThreadA,ThreadB,ThreadC,按以下步骤执行操作。
1.ThreadA调用take(),获取到锁,因为队列中没有元素,ThreadA调用notEmpty.await()进入等待队列,现在同步器的情况如下:
2.ThreadB调用put()方法,往队列中添加元素,获取到锁,调用方法enqueue(e)往队列中添加元素时,cpu时间片用完了,现在锁还是在ThreadB手上
3.线程ThreadC 调用take(),因为锁还在ThreadB手上,所以ThreadC 进入同步队列尾部:如下图所示
4.线程ThreadB完成往队列中放入对象的操作,并且调用notEmpty.signal()唤醒了ThreadA线程,ThreadA被唤醒,调用acquireQueued(node, savedState)方法加入到锁竞争,但锁还在ThreadB手上,锁竞争失败,最终ThreadA进入同步队列尾部,现在同步队列如下图所示:
5.ThreadB指行lock.unlock()释放锁,并且唤醒下一个节点,下一个节点是node2,ThreadC拿到了锁,比先来的ThreadA先获取到同步队列的数据。
小结:所以公平性仅仅是指获取锁的公平性,ThreadA先获取锁,调用await()方法将锁释放,ThreadA自身进入等待状态,并且加入到condiction队列,ThreadB此时进来拿到了锁,调用方法enqueue(e)往队列添加元素时,时间片用完了,ThreadB挂起,ThreadC进来争夺锁,然而锁还被ThreadB持有,ThreadC进入同步队列尾部,此时ThreadB继续执行方法,先调用signal()方法唤醒了ThreadA,ThreadA被唤醒继续争夺锁,锁争夺失败,进入同步队列尾部。按调用lock()方法的顺序是ThreadC先调用lock()方法,所以理应是ThreadC先去同步队列拿取数据。
5.2.2 LinkedBlockingQueue
LinkedBlockingQueue 是一个用链表实现的阻塞队列。此队列的默认和最大长度为 Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。
5.2.3 PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现 compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。代码示例如下:
public class PriorityBlockingQueueUserCase {
private static PriorityBlockingQueue<Integer> priorityBlockingQueue
= new PriorityBlockingQueue(10, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
public static void main(String[] args) throws InterruptedException {
priorityBlockingQueue.put(3);
priorityBlockingQueue.put(55);
priorityBlockingQueue.put(2);
// 返回 2
Integer take = priorityBlockingQueue.take();
System.out.println(take);
}
}
5.2.4 DelayQueue
DelayQueue 是一个支持延时获取元素的无界阻塞队列。队列使用 PriorityQueue 来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
5.2.5 SynchronousQueue
SynchronousQueue 是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个take 操作,否则不能继续添加元素。
它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法可以创建公平性访问的 SynchronousQueue,如果设置为 true,则等待的线程会采用先进先出的顺序访问队列。
SynchronousQueue synchronousQueue = new SynchronousQueue(true);
5.2.6 LinkedTransferQueue
LinkedTransferQueue 是一个由链表结构组成的无界阻塞 TransferQueue 队列。相对于其他阻塞队列,LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
- transfer 方法
如果当前有消费者正在等待接收元素(消费者使用 take()方法或带时间限制的 poll()方法时),transfer 方法可以把生产者传入的元素立刻 transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的 tail 节点,并等到该元素被消费者消费了才返回。transfer 方法的关键代码如下。
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
第一行代码是试图把存放当前元素的 s 节点作为 tail 节点。第二行代码是让 CPU 自旋等待消费者消费元素。因为自旋会消耗 CPU,所以自旋一定的次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
- tryTransfer 方法
tryTransfer 方法是用来试探生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回 false。和 transfer 方法的区别是 tryTransfer 方法无论消费者是否接收,方法立即返回,而 transfer 方法是必须等到消费者消费了才返回。对于带有时间限制的 tryTransfer(E e,long timeout,TimeUnit unit)方法,试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回true。
5.2.7 LinkedBlockingDeque
LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列。所谓双向队列指的是可以从队列的两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了addFirst、addLast、offerFirst、offerLast、peekFirst 和 peekLast 等方法,以 First 单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法 add 等同于addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 JDK 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。
5.3 阻塞队列的实现原理
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。关键点就是上面介绍 Condition类,采用等待通知的机制来实现。
两端插入和移出元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque 多了addFirst、addLast、offerFirst、offerLast、peekFirst 和 peekLast 等方法,以 First 单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以 Last 单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。另外,插入方法 add 等同于addLast,移除方法 remove 等效于 removeFirst。但是 take 方法却等同于 takeFirst,不知道是不是 JDK 的 bug,使用时还是用带有 First 和 Last 后缀的方法更清楚。在初始化 LinkedBlockingDeque 时可以设置容量防止其过度膨胀。另外,双向阻塞队列可以运用在“工作窃取”模式中。
5.3 阻塞队列的实现原理
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。关键点就是上面介绍 Condition类,采用等待通知的机制来实现。
参考:《Java并发编程的艺术》