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 && nanos <= 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;
- }
- }
- }
Java并发包之同步队列SynchronousQueue理解的更多相关文章
- Java并发编程(您不知道的线程池操作), 最受欢迎的 8 位 Java 大师,Java并发包中的同步队列SynchronousQueue实现原理
Java_并发编程培训 java并发程序设计教程 JUC Exchanger 一.概述 Exchanger 可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchan ...
- Java并发包——线程同步和锁
Java并发包——线程同步和锁 摘要:本文主要学习了Java并发包里有关线程同步的类和锁的一些相关概念. 部分内容来自以下博客: https://www.cnblogs.com/dolphin0520 ...
- 并发编程-concurrent指南-阻塞队列-同步队列SynchronousQueue
SynchronousQueue:同步Queue,属于线程安全的BlockingQueue的一种,此队列设计的理念类似于"单工模式",对于每个put/offer操作,必须等待一个t ...
- 【转载】阻塞队列之三:SynchronousQueue同步队列 阻塞算法的3种实现
一.SynchronousQueue简介 Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除 ...
- 阻塞队列之三:SynchronousQueue同步队列 阻塞算法的3种实现
一.SynchronousQueue简介 Java 6的并发编程包中的SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除 ...
- Java并发包
刚看到一篇总结的比较全的JUC包总结,转载如下: 1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.con ...
- Java并发包源码学习系列:阻塞队列实现之SynchronousQueue源码解析
目录 SynchronousQueue概述 使用案例 类图结构 put与take方法 void put(E e) E take() Transfer 公平模式TransferQueue QNode t ...
- Java并发包源码学习系列:CLH同步队列及同步资源获取与释放
目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...
- Java并发包源码学习系列:阻塞队列BlockingQueue及实现原理分析
目录 本篇要点 什么是阻塞队列 阻塞队列提供的方法 阻塞队列的七种实现 TransferQueue和BlockingQueue的区别 1.ArrayBlockingQueue 2.LinkedBloc ...
随机推荐
- 某公司基于FineBI数据决策平台的试运行分析报告
一.数据平台的软硬件环境 二.组织机构和权限体系 组织机构:平台中已集成一套组织机构,可以建立部门.人员.也可以与现有系统的组织机构集成,将组织机构导入到平台中. 功能权限:通过配置功能点URL的方式 ...
- CentOS 6.X启动流程
CentOS 6.X启动流程 /boot分区 启动引导程序是默认可以识别boot分区的.因此在系统还无法加载硬盘的时候,boot分区是可以识别的! initramfs内存文件系统 CentOS 6.x ...
- SpriteBuilder中pivot关节中的Collide bodies属性
在SpriteBuilder中,pivot类型的关节表示两个物体围绕一个中心旋转运动的关节,也称之为pin关节. 默认情况下Collide bodies是不选的.因为在大多数情况下你不希望pivot连 ...
- 2DSprite添加Light照射(Unity3D开发之十六)
猴子原创,欢迎转载.转载请注明: 转载自Cocos2Der-CSDN,谢谢! 原文地址: http://blog.csdn.net/cocos2der/article/details/45534245 ...
- Java泛型type体系
最近看开源代码,看到里面很多Java泛型,并且通过反射去获取泛型信息.如果说要看懂泛型代码,那还是比较容易,但是如果要自己利用泛型写成漂亮巧妙的框架,那必须对泛型有足够的了解.所以这两三天就不在不断地 ...
- unix设计哲学
说到Unix为我们所带来的软件开发的哲学,我必需要说一说.Unix遵循的原则是KISS(Keep it simple, stupid).在http://en.wikipedia.org/wiki/Un ...
- c# http请求ajax页面
我们在用Http请求的时候,某些页面是ajax加载的,所以请求过来的页面数据不完整.也就是说ajax局部加载数据的地方,我们请求不到,这时候该怎么办呢? WebDriver+phantomjs 这两个 ...
- MVC3 项目总结
验证 Validation 多样化验证规则 http://www.cnblogs.com/xling/archive/2012/07/11/2587002.html 最常见的验证方式是:在实体的属性上 ...
- 轻松解决oracle11g 空表不能exp导出的问题
轻松解决oracle11g 空表不能exp导出的问题 [引用 2012-9-22 18:06:36] 字号:大 中 小 oracle11g的新特性,数据条数是0时不分配segment,所以就不 ...
- 论MVC中的传值
2个页面分别为Father.cshtml.Child.cshtml 2个控制器分别为FatherController.cs.ChildController.cs 1个js,为Father.js 一.F ...