阻塞队列 SynchronousQueue 详解
文章目录
一、SynchronousQueue介绍
-
这个阻塞队列和其他的阻塞队列有很大的区别 , 队列的概念一般是要进行存储数据的
而这个阻塞队列并不会存储数据
, 这个队列会把生产者和消费者放到队列中让他们去配对 -
当存储了一个生产者到SynchronousQueue队列之后,生产者会阻塞(例如poll(time,unit))或者不允许阻塞直接进行匹配例如poll方法
生产者最终会有以下几种结果
<1> 如果生产者在阻塞期间有消费者来匹配 , 生产者就会将绑定的消息交给消费者
<2> 生产者等待到阻塞结束、或者生产者不允许阻塞(poll方法) , 那么当前生产者直接失败
<3> 生产者在阻塞期间,被线程中断了,那么同样的这个生产者直接失败
同理,消费者和生产者一样 , 当消费者把自己扔到SynchronousQueue队列中时要去匹配生产者,其效果和上边生产者的三种状态类似 -
从上边2可以得出结论
<1.> 生产者和消费者不会像之前的阻塞队列那样 会向底层封装的存储数据的容器打交道,而是生产者和消费者直接进行通信,不经过阻塞队列的容器
<2.> 相当于是生产者直接把数据给到消费者 -
探究生产者调用不同方法的区别
生产者
:
<1.> offer()方法 : 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
如果没有消费者正在等待生产者的消息 , 那么就直接返回,结束方法
<2.> offer(time,unit): 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
如果没有消费者正在等待生产者的消息 , 那么该生产者就阻塞time时间 , 之后如果还没有那么直接返回,结束方法
❤️.> put() : 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
如果没有消费者正在等待生产者的消息 , 那么该生产者就死等
消费者
: poll()、poll(time,unit)、take() 方法,和上边生产者道理一致 - 测试SynchronousQueue Demo例子
/**
* @author xqh, 987072248@qq.com
* @date: 2022_12_13 _ 21:49__星期二
*/
public class SynchronousQueueDemo {
private static SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
public static void main(String[] args) throws InterruptedException {
// notHaveConsumer();
// produceWaitConsumer();
consumerWaitProduce();
}
/**
* 1. 只有生产者生产消息、没有消费者来配对
*/
private static void notHaveConsumer(){
String message = "消息A";
new Thread(() -> {
boolean offerFlag = synchronousQueue.offer(message);
System.out.println(offerFlag); // false
}).start();
}
/**
* 生产者等待消费者
* 2. 生产者等待一段时间 等到了消费者来消费数据
* @throws InterruptedException
*/
private static void produceWaitConsumer() throws InterruptedException {
String message = "消息B";
new Thread(() -> {
boolean offerFlag = false;
try {
// 加入当前生产者 等待1s 消费者加入
offerFlag = synchronousQueue.offer(message , 1 , TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(offerFlag); // true
}).start();
// 保证生产者先等待消费者
Thread.sleep(10);
new Thread(() -> {
String poll = synchronousQueue.poll();
System.out.println("匹配消费者获取到的数据为-->" + poll); // 匹配消费者获取到的数据为-->消息B
}).start();
}
/**
* 消费者等待生产者
* 3. 消费者等待一段时间 等待了生产者传入数据
*/
private static void consumerWaitProduce() throws InterruptedException {
String message = "消息C";
new Thread(() -> {
String pollData = null;
try {
// 加入当前消费者 等待1s 生产者加入
pollData = synchronousQueue.poll(1 , TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("匹配消费者获取到的数据为-->" + pollData); // 匹配消费者获取到的数据为-->消息C
}).start();
// 保证先让消费者等待生产者
Thread.sleep(10);
new Thread(() -> {
boolean offerFlag = synchronousQueue.offer(message);
System.out.println(offerFlag); // true
}).start();
}
}
二、SynchronousQueue核心属性
2.1 抽象内部类 Transferer
其内部的transfer方法 就是当前阻塞队列 生产者和消费者在 读写数据时要使用的方法
abstract static class Transferer<E> {
// 生产者和消费者都要用这个方法执行逻辑
/**
* 1. 生产者在调用这个方法时 , 第一个参数e 正常传参 表示本次生产者要提供的数据
*
* 2. 消费者在调用这个方法时 , 第一个参数e会传递null
*
* @param timed 表示当前线程 是否需要阻塞 true为需要阻塞
* @param nanos 根据timed如果其为true 则nanos为等待的时长
*/
abstract E transfer(E e, boolean timed, long nanos);
}
2.2 构造方法
- Transferer抽象类 在 SynchronousQueue中提供了两种具体实现 TransferStack、TransferQueue
- 在创建SynchronousQueue阻塞队列时 可以传入一个公平或者非公平的boolean参数 非公平则是TransferQueue、公平则是TransferQueue
- TransferStack : 栈结构
- 栈结构不存在 tail 指针
- TransferQueue : 队列结构
- 队列结构存在 head和tail 并且默认指向的是一个伪节点
// 默认就是非公平实现即 TransferStack
public SynchronousQueue() {
this(false);
}
// 可以指定公平或者非公平
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
- 测试使用 公平和非公平的SynchronousQueue
/**
* ---> 测试SynchronousQueue底层的公平(TransferQueue)和非公平(TransferStack)实现
*
* @author xqh , 987072248@qq.com
* @data 2022-12-15 12:42:22
*/
public class SynchronousQueueSequenceDemo {
public static void main(String[] args) throws InterruptedException {
// 公平
entryTest(true);
Thread.sleep(2000);
System.out.println("===================");
// 不公平
entryTest(false);
/**
输出结果:
消费者A拿到-->生产者A携带数据A
消费者B拿到-->生产者B携带数据B
消费者C拿到-->生产者C携带数据C
===================
消费者A拿到-->生产者C携带数据C
消费者B拿到-->生产者B携带数据B
消费者C拿到-->生产者A携带数据A
*/
}
private static void entryTest(boolean fair) throws InterruptedException {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>(fair);
testMain(synchronousQueue);
}
/**
* 公平的生产者消费者匹配结构 , 队列结构 先进先出
*/
private static void testMain(SynchronousQueue<String> fairQueue) throws InterruptedException {
// 需要让生产者等待一会消费者 不然生产者会直接return null
Thread produceA = new Thread(() -> {
try {
fairQueue.offer("生产者A携带数据A", 2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者A");
Thread produceB = new Thread(() -> {
try {
fairQueue.offer("生产者B携带数据B", 2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者B");
Thread produceC = new Thread(() -> {
try {
fairQueue.offer("生产者C携带数据C", 2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者C");
// 按照顺序 生产者添加顺序 A -> B -> C
produceA.start();
Thread.sleep(1);
produceB.start();
Thread.sleep(1);
produceC.start();
Thread.sleep(100);
Thread consumerA = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "拿到-->" + fairQueue.poll());
},"消费者A");
Thread consumerB = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "拿到-->" + fairQueue.poll());
},"消费者B");
Thread consumerC = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "拿到-->" + fairQueue.poll());
},"消费者C");
// 按照顺序取数据 消费者取数据顺序 A -> B -> C
consumerA.start();
Thread.sleep(1);
consumerB.start();
Thread.sleep(1);
consumerC.start();
}
}
三、SynchronousQueue的Transfer源码分析
3.1 TransferQueue 源码分析
3.1.1 QNode 类信息
static final class QNode {
// 当前QNode节点的 下一个节点
volatile QNode next;
// 如果是生产者 item 存放的就是携带的数据
// 如果是消费者 item 则为null
volatile Object item;
// 无论是生产者还是消费者 ,存放的就是当前线程Thread
volatile Thread waiter;
// 用来区分当前节点是生产者还是消费者 根据item是否有值来判断即可
final boolean isData;
/**
* 创建QNode的唯一方式
* @param item 存放数据 生产者放入数据 消费者放入null
* @param isData 当前节点是生产者吗? isDate为true则是生产者 、 反之为消费者
*/
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
// =========================================
// ====== 后边全是 CAS 相关方法 省略掉 =========
// =========================================
}
3.1.2 TransferQueue核心属性、构造器
// 当前 Queue 队列的 head指针 表示队列头部的那个节点(永远指向的都是那个伪节点)
transient volatile QNode head;
// 当前 Queue 队列的 tail指针 表示队列尾部的那个节点
transient volatile QNode tail;
transient volatile QNode cleanMe;
// 只有这一种 无参构造方式
TransferQueue() {
// 初始化一个伪节点 , 这个伪节点不携带数据 不是生产者节点
QNode h = new QNode(null, false); // initialize to dummy node.
// 初始化时 head 和 tail 同时指向该伪节点
head = h;
tail = h;
}
3.1.3 transfer 具体实现
E transfer(E e, boolean timed, long nanos) {
// 声明一个QNode变量 , 将当前 生产者或者消费者封装为QNode
QNode s = null;
// isDate 为true 说明是生产者 反之为消费者
boolean isData = (e != null);
// 因为执行这个方法并没有使用锁 使用死循环多次尝试CAS操作
for (;;) {
QNode t = tail;
QNode h = head;
// 这里做的兼容性判断 t和h在初始化伪节点时可能会出现指令重排的现象(一般不会)
if (t == null || h == null)
continue;
/**
* 1. h == t , 说明当前队列中没有任何生产者或者消费者 , 此时h和t指向的都是那个伪节点
*
* 2. t.isData == isData , 走到这个逻辑说明队列中有节点 同时当前节点的类型 和队列中的节点类型是同一种类型(类型是:生产者或者消费者)
*
* 所以这个 if 里边的逻辑就是把当前节点进入到队列里边
*/
if (h == t || t.isData == isData) {
// 拿到 tail 的next节点
// 如果没有并发现象出现 tn 一定是个null , 但是这个方法并没有使用锁 所以会出现并发问题
QNode tn = t.next;
// ======== 下面处理 并发问题 =============
// t != tail 说明出现了并发问题 , 重新循环执行
if (t != tail)
continue;
// tail有next节点 , 则说明有其他线程并发添加了一个节点
if (tn != null) {
// 帮那个并发线程 修改tail的指向
advanceTail(t, tn);
// 有并发问题 当前线程则重新 循环执行
continue;
}
// ======= 走到这里 说明未出现并发问题 ==========
// 需要阻塞 并且 阻塞时间 <= 0 那么直接返回 null
// 生产者的offer() 方法和 消费者的 poll() 方法执行到这里进去直接返回null
if (timed && nanos <= 0)
return null;
// ======== 走到这里 说明当前线程可以阻塞 ========
// 由于下方有 continue , 这里就是为了防止多次初始化s
if (s == null)
s = new QNode(e, isData);
// CAS更新tail的next节点为本次新增的s节点
if (!t.casNext(null, s))
// CAS失败的话 重新执行for循环
continue;
// 将tail指针指向本次新增的QNode节点 s
advanceTail(t, s);
// 到这里说明本次构建的QNode已经进入队列了
// 这里就需要挂起当前 生产者或消费者了
Object x = awaitFulfill(s, e, timed, nanos);
// 如果元素和 节点相等 说明这个节点取消了
if (x == s) {
// 清空当前节点 , 将上一个节点的next指向当前节点的next 绕过取消的节点
clean(t, s);
return null;
}
// 判断当前节点是否还在队列中
if (!s.isOffList()) {
// 将当前s设置为新的head节点
advanceHead(t, s);
// x != null 拿到了数据 , 说明当前是消费者
if (x != null)
// 将本次节点的数据item设置为x
s.item = s;
// 线程属性设置为null
s.waiter = null;
}
// 返回数据
return (x != null) ? (E)x : e;
}
/**
* 通过上边的 if 分析 , 那么这个else 里边的逻辑就是 匹配与当前节点对立的节点 (当前是生产者匹配队列里边的消费者 , 当前是消费者匹配队列里边的生产者)
* 匹配节点就要从 head 还是操作了
*/
else {
// 拿到head的next节点 , 有效节点(head是伪节点)
QNode m = h.next;
// 同样的做并发判断 如果 tail、head、head的next节点发生了变化 那么就需要重新走for循环
if (t != tail || m == null || h != head)
continue;
// 获取有效节点的item数据
Object x = m.item;
/**
* 1. isData == (x != null) 这个条件如果满足的话 则说明出现了并发问题 , 出现生产者去匹配生产者现象
* 2. x == m , 排队的节点取消的话 , 就会把 QNode的item指向自己节点 , 满足条件说明这个有效节点取消了
* 3. !m.casItem(x, e) , 前边两个都没满足 也就是没有异常 那么就可以交换数据了 , 把队列中第一个有效节点替换为本次传入的数据e
* 如果e有值说本次匹配的是消费者则把消费者的item更改为e , 反之同理
* 如果CAS交换数据失败了 , 那么就
*/
if (isData == (x != null) || x == m || !m.casItem(x, e)) {
// 设置新的 head为head的next节点m , 重新执行for循环
advanceHead(h, m);
continue;
}
// 替换head为m
advanceHead(h, m); // successfully fulfilled
// 唤醒原来的head.next节点的线程
LockSupport.unpark(m.waiter);
// 队列中是生产者则直接返回x , 当前是消费者 , 则返回本次消费者交换来的数据 x
// 反之 x == null 则队列中是消费者 , 当前是生产者 则返回本次生产者提供的数据e
return (x != null) ? (E)x : e;
}
}
}