Java 从condition到阻塞队列

1.Condition 接口与synchronized的比较

  任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、 wait(long timeout)、notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。通过对比 Object 的监视器方法和 Condition 接口,可以更详细地了解 Condition 的特性,对比项与结果如表 1-1所示。

表1-1 Object的监视器方法与Condition接口的对比
对比项Object Monitor MethodsCondition
前置条件获取对象的锁调用Lock.lock()获取锁
调用Lock.newCondition() 获取Condition对象
调用方式直接调用
如:object.wait()
直接调用
如:condition.await()
等待队列个数一个多个
当前线程释放锁并进入等待状态支持支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态到将来的某个时间不支持支持
唤醒在等待队列中的的一个线程支持支持
唤醒等待队列中的全部线程支持支持

2.Condition 接口与示例

  Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁。Condition 对象是由 Lock 对象(调用 Lock 对象的newCondition()方法)创建出来的,换句话说,Condition 是依赖 Lock 对象的。Condition 的使用方式比较简单,需要注意在调用方法前获取锁,使用方式如代码清单 1-1 所示。

代码清单1-1 CondictionTest.java
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 所示。

表1-2 Condition的常用方法以及描述
方法名称描述
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 所示。

代码清单 1-2 SimpleBoundedQueue.java
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 所示。

图1-1 等待队列的基本结构

  等待队列的基本结构如图所示,Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

  在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如图1-2所示。

!(https://img-blog.csdnimg.cn/057a606b6faa489cb1e0950ea06fa6ef.png)

图1-2 同步队列与等待队列

  如图所示,Condition 的实现是同步器的内部类,因此每个 Condition 实例都能够访问同步器提供的方法,相当于每个 Condition 都拥有所属同步器的引用。

4.2 等待

  调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁。如果从队列(同步队列和等待队列)的角度看 await()方法,当调用 await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。

  Condition 的 await()方法,如代码清单 1-3 所示。

代码清单 1-3 java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#await()
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()方法把当前线程构造成一个新的节点并将其加入等待队列中。
在这里插入图片描述

图1-3 当前线程加入等待队列

4.3 通知

  调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition 的 signal()方法,如代码清单 1-4 所示

代码清单 1-4 java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject#signal
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 所示。

在这里插入图片描述

图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 所示。

表1-4 插入和移除操作的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

代码清单 1-5 BlockingQueueUseCase.java
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 方法。

  1. 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()方法来暂停当前正在执行的线程,并执行其他线程。

  1. 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并发编程的艺术》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值