Java并发包之同步队列SynchronousQueue理解

时间:2023-03-09 00:44:32
Java并发包之同步队列SynchronousQueue理解

1 简介

SynchronousQueue是这样一种阻塞队列,其中每个put必须等待一个take,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行peek,因为仅在试图要取得元素时,该元素才存在,除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素,也不能迭代队列,因为其中没有元素可用于迭代。队列的头是尝试添加到队列中的首个已排队线程元素,如果没有已排队线程,则不添加元素并且头为 null。
对于其他Collection方法(例如 contains),SynchronousQueue作为一个空集合,此队列不允许 null 元素。
同步队列类似于CSP和Ada中使用的rendezvous信道。它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。
对于正在等待的生产者和使用者线程而言,此类支持可选的公平排序策略,默认情况下不保证这种排序。
但是,使用公平设置为true所构造的队列可保证线程以FIFO的顺序进行访问。公平通常会降低吞吐量,但是可以减小可变性并避免得不到服务。

2 使用示例

 import static org.junit.Assert.assertEquals;

 import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; /**
* synchronousqueue的使用场景 ==== 线程间共享元素
* 假设有两个线程,一个生产者和一个消费者,当生产者设置一个共享变量的值时,我们希望向消费者线程
* 发出这个信号,然后消费者线程将从共享变量取值。
* @author ko
*
*/
public class Sqt { /**
* 利用AtomicInteger+CountDownLatch实现
*/
@Test
public void doingByCountDownLatch(){
ExecutorService executor = Executors.newFixedThreadPool(2);
AtomicInteger sharedState = new AtomicInteger();// 共享变量值
// 协调这两个线程,以防止情况当消费者访问共享变量值
CountDownLatch countDownLatch = new CountDownLatch(1); // 生产商将设置一个随机整数到sharedstate变量,并countdown()方法,
// 信号给消费者,它可以从sharedstate取一个值
Runnable producer = () -> {// 这好像是java8的匿名内部类的新写法
Integer producedElement = ThreadLocalRandom.current().nextInt();
sharedState.set(producedElement);
System.out.println("生产者给变量设值:"+producedElement);
countDownLatch.countDown();
}; // 消费者会等待countdownlatch执行到await()方法,获取许可后,再从生产者里获取变量sharedstate值
Runnable consumer = () -> {
try {
countDownLatch.await();
Integer consumedElement = sharedState.get();
System.out.println("消费者获取到变量:"+consumedElement);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}; executor.execute(producer);
executor.execute(consumer);
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
assertEquals(countDownLatch.getCount(), 0);
} /**
* 仅使用SynchronousQueue就可以实现
*/
@Test
public void doingBySynchronousQueue(){
ExecutorService executor = Executors.newFixedThreadPool(2);
SynchronousQueue<Integer> queue = new SynchronousQueue<>(); // 生产者
Runnable producer = () -> {
Integer producedElement = ThreadLocalRandom.current().nextInt();
try {
queue.put(producedElement);
System.out.println("生产者设值:"+producedElement);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}; // 消费者
Runnable consumer = () -> {
try {
Integer consumedElement = queue.take();
System.out.println("消费者取值:"+consumedElement);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}; executor.execute(producer);
executor.execute(consumer);
try {
executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
assertEquals(queue.size(), 0);
}
}

3 实现原理

阻塞队列的实现方法有许多。

3.1 阻塞算法实现

阻塞算法实现通常在内部采用一个锁来保证多个线程中的put()和take()方法是串行执行的。采用锁的开销是比较大的,还会存在一种情况是线程A持有线程B需要的锁,B必须一直等待A释放锁,即使A可能一段时间内因为B的优先级比较高而得不到时间片运行。所以在高性能的应用中我们常常希望规避锁的使用。

 public class NativeSynchronousQueue<E> {
boolean putting = false;
E item = null; public synchronized E take() throws InterruptedException {
while (item == null)
wait();
E e = item;
item = null;
notifyAll();
return e;
} public synchronized void put(E e) throws InterruptedException {
if (e==null) return;
while (putting)
wait();
putting = true;
item = e;
notifyAll();
while (item!=null)
wait();
putting = false;
notifyAll();
}
}

3.2 信号量实现

经典同步队列实现采用了三个信号量,代码很简单,比较容易理解。

 public class SemaphoreSynchronousQueue<E> {
E item = null;
Semaphore sync = new Semaphore(0);
Semaphore send = new Semaphore(1);
Semaphore recv = new Semaphore(0); public E take() throws InterruptedException {
recv.acquire();
E x = item;
sync.release();
send.release();
return x;
} public void put (E x) throws InterruptedException{
send.acquire();
item = x;
recv.release();
sync.acquire();
}
}

在多核机器上,上面方法的同步代价仍然较高,操作系统调度器需要上千个时间片来阻塞或唤醒线程,而上面的实现即使在生产者put()时已经有一个消费者在等待的情况下,阻塞和唤醒的调用仍然需要。

3.3 Java 5实现

 public class Java5SynchronousQueue<E> {
ReentrantLock qlock = new ReentrantLock();
Queue waitingProducers = new Queue();
Queue waitingConsumers = new Queue(); static class Node extends AbstractQueuedSynchronizer {
E item;
Node next; Node(Object x) { item = x; }
void waitForTake() { /* (uses AQS) */ }
E waitForPut() { /* (uses AQS) */ }
} public E take() {
Node node;
boolean mustWait;
qlock.lock();
node = waitingProducers.pop();
if(mustWait = (node == null))
node = waitingConsumers.push(null);
qlock.unlock(); if (mustWait)
return node.waitForPut();
else
return node.item;
} public void put(E e) {
Node node;
boolean mustWait;
qlock.lock();
node = waitingConsumers.pop();
if (mustWait = (node == null))
node = waitingProducers.push(e);
qlock.unlock(); if (mustWait)
node.waitForTake();
else
node.item = e;
}
}

Java 5的实现相对来说做了一些优化,只使用了一个锁,使用队列代替信号量也可以允许发布者直接发布数据,而不是要首先从阻塞在信号量处被唤醒。

3.4 Java 6实现

Java 6的SynchronousQueue的实现采用了一种性能更好的无锁算法 — 扩展的“Dual stack and Dual queue”算法。性能比Java5的实现有较大提升。竞争机制支持公平和非公平两种:非公平竞争模式使用的数据结构是后进先出栈(Lifo Stack);公平竞争模式则使用先进先出队列(Fifo Queue),性能上两者是相当的,一般情况下,Fifo通常可以支持更大的吞吐量,但Lifo可以更大程度的保持线程的本地化。

代码实现里的Dual Queue或Stack内部是用链表(LinkedList)来实现的,其节点状态为以下三种情况:

持有数据 – put()方法的元素

持有请求 – take()方法

这个算法的特点就是任何操作都可以根据节点的状态判断执行,而不需要用到锁。

其核心接口是Transfer,生产者的put或消费者的take都使用这个接口,根据第一个参数来区别是入列(栈)还是出列(栈)。

 /**
* Shared internal API for dual stacks and queues.
*/
static abstract class Transferer {
/**
* Performs a put or take.
*
* @param e if non-null, the item to be handed to a consumer;
* if null, requests that transfer return an item
* offered by producer.
* @param timed if this operation should timeout
* @param nanos the timeout, in nanoseconds
* @return if non-null, the item provided or received; if null,
* the operation failed due to timeout or interrupt --
* the caller can distinguish which of these occurred
* by checking Thread.interrupted.
*/
abstract Object transfer(Object e, boolean timed, long nanos);
}

TransferQueue实现如下(摘自Java 6源代码),入列和出列都基于Spin和CAS方法:

 /**
* Puts or takes an item.
*/
Object transfer(Object e, boolean timed, long nanos) {
/* Basic algorithm is to loop trying to take either of
* two actions:
*
* 1. If queue apparently empty or holding same-mode nodes,
* try to add node to queue of waiters, wait to be
* fulfilled (or cancelled) and return matching item.
*
* 2. If queue apparently contains waiting items, and this
* call is of complementary mode, try to fulfill by CAS'ing
* item field of waiting node and dequeuing it, and then
* returning matching item.
*
* In each case, along the way, check for and try to help
* advance head and tail on behalf of other stalled/slow
* threads.
*
* The loop starts off with a null check guarding against
* seeing uninitialized head or tail values. This never
* happens in current SynchronousQueue, but could if
* callers held non-volatile/final ref to the
* transferer. The check is here anyway because it places
* null checks at top of loop, which is usually faster
* than having them implicitly interspersed.
*/ QNode s = null; // constructed/reused as needed
boolean isData = (e != null); for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // saw uninitialized value
continue; // spin if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
if (t != tail) // inconsistent read
continue;
if (tn != null) { // lagging tail
advanceTail(t, tn);
continue;
}
if (timed &amp;&amp; nanos &lt;= 0) // can't wait
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s)) // failed to link in
continue; advanceTail(t, s); // swing tail and wait
Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) { // wait was cancelled
clean(t, s);
return null;
} if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null)? x : e; } else { // complementary-mode
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
} advanceHead(h, m); // successfully fulfilled
LockSupport.unpark(m.waiter);
return (x != null)? x : e;
}
}
}