并发()——SynchronousQueue源码解析

并发()——SynchronousQueue源码解析_第1张图片

概述

SynchronousQueue 实际上不是一个真正的队列,因为它不会为队列中的元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移除队列。

SynchronousQueue不像ArrayBlockingQueue或者LinkedBlockingQueue,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试图取走的时候才可能存在,不取走而只想看一下是否有数据是不可以的。队列的头元素是第一个要输入数据的线程,而不是要交换的数据。数据是在配对的生产者与消费者之间直接传递,并不会将数据缓冲到队列中。

SynchronousQueue的应用场景

Executors.newCachedThreadPool() 就是使用了 SynchronousQueue,即SynchronousQueue作为newCachedThreadPool的工作队列(workQueue)。这个线程池在新任务到来时需要创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒之后就会被回收。

SynchronousQueue的公平模式源码

SynchronousQueue内部是通过Transferer实现的,具体分为两个Transferer,分别是TransferStack和TransferQueue,两者的差别在于是否公平。

并发()——SynchronousQueue源码解析_第2张图片

并发()——SynchronousQueue源码解析_第3张图片

并发()——SynchronousQueue源码解析_第4张图片

上面的transfer 方法的主逻辑:

这是 producer / consumer 的主方法,主要分为两种情况:

  • 若队列为空 / 队列中的尾结点和自己的类型相同,则添加 node 到队列中,直到 timeout / interrupt / 其他线程和这个线程匹配。

    • timeout / interrupt awaitFulfill 方法返回的是 node本身
    • 匹配成功的话,要么返回 null (producer 返回的), 或真正的传递值(consumer返回的)
  • 队列不为空,且队列的 head.next 结点是当前节点匹配的结点,进行数据的传递匹配,并且通过 advanceHead方法帮助先前 block 的结点 dequeue

下面具体讲解一下上面的transfer方法的执行过程:首先我们要知道,在transfer 中,把操作分为两种,一种是入队put操作,另一种是出队 take操作,入队的时候会创建 data 结点,值为data。 出队的时候会创建一个request 结点,值为null 。

  • 【步骤一】put 和 take 操作都会调用该方法,区别在于,put操作的时候 e 值为数据 data, take 的时候 e 值为 null

  • 【步骤二】如果 h == t ,即队列为空,或者当前队列尾部的数据类型和调用该方法的数据类型一致:比如当前队列为空,第一次来了一个入队请求,这个时候队列就会创建一个data结点,如果第二次又来了一个入队请求(和第一次也就是队列尾部的数据类型一致,都是入队请求),这时候队列会创建出第二个data结点,并形成一个链表。同理,如果刚开始来了request请求,也会入队,之后如果继续来了一个request请求,也会继续如队列。

  • 【步骤三】满足【步骤二】的条件,就会进入步骤三,中间会有一些一致性检查,这个检查的目的是为了避免产生并发冲突。步骤三会创建出一个结点,根据e值类型的不同,可能是data结点或者是request结点。

  • 【步骤四】把步骤三中创建的结点通过cas的方式设置到队列尾部

  • 【步骤五】把tail 通过 cas 的方式修改为步骤三中新建立的 s 结点

  • 【步骤六】调用方法 awaitFulFill 进行等待,如果步骤三中创建的是data结点,那么就会等待来一个request结点,如果步骤三种创建的是request结点,那么就会等待来一个data结点。

    • 【6.1】放入队列之后就开始进行循环判断
    • 【6.2】终止条件是节点的值被修改,具体如果是data结点,那么会被修改成 null, 如果是 request 结点,那么会被修改成 data 值。 这个修改是在步骤九中由相对的请求(如果创建的是data结点,那么就由request请求来进行修改,反之亦然)来做的。如果一直没有相对的请求过来,那么结点的值就一直不会被修改,这样就跳不出循环。
    • 【6.3】如果没有被修改,那么就需要进入park休眠,等待步骤九进行修改后再通过unpark进行唤醒,唤醒之后就会判断结点值被修改从而返回。
  • 【步骤七】如果在插入一个结点的时候,不满足不走而的条件,也就是队列不为空并且尾部结点和当前要插入的结点类型不一样(这表示是一个相对请求),那么就会往下执行

  • 【步骤八】由于是队列,先进先出,所以会去的队列里面的第一个结点,也就是 h.next

  • 【步骤九】把步骤八中取出的结点的值通过cas的方式设置成新来结点的e的值,这样就成功的满足了 6.2 中的终止条件

  • 【步骤十】将 head 结点往后移动,这样就把第一个结点成功的出队

  • 【步骤十一】每个节点都保存了对应的操作线程,将步骤八中节点对应的线程进行唤醒,这样步骤6.3中处于休眠的线程就醒来了,然后继续进行for循环,进而判断 6.2 终止条件满足,于是返回。

并发()——SynchronousQueue源码解析_第5张图片

SynchronousQueue 的应用

import java.util.Random;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;

public class SynchronousQueueExample {
	
	static class SynchronousQueueProducer implements Runnable {
		protected BlockingQueue blockingQueue;
		final Random random = new Random();
		
		
		public SynchronousQueueProducer(BlockingQueue blockingQueue) {
			super();
			this.blockingQueue = blockingQueue;
		}

		@Override
		public void run() {
			while(true) {
				
				try {
					String data = UUID.randomUUID().toString();
					System.out.println("put: " + data);
					blockingQueue.put(data);
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	
	static class SynchronousQueueConsumer implements Runnable {
		protected BlockingQueue blockingQueue;
		
		
		public SynchronousQueueConsumer(BlockingQueue blockingQueue) {
			super();
			this.blockingQueue = blockingQueue;
		}
		

		@Override
		public void run() {
			while(true) {
				try {
					String data = blockingQueue.take();
					System.out.println(Thread.currentThread().getName() + "take :" + data);
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		final BlockingQueue synchronousQueue = new SynchronousQueue<>();
		SynchronousQueueProducer producer = new SynchronousQueueProducer(synchronousQueue);
		
		SynchronousQueueConsumer consumer1 = new SynchronousQueueConsumer(synchronousQueue);
		SynchronousQueueConsumer consumer2 = new SynchronousQueueConsumer(synchronousQueue);
		
		new Thread(producer).start();
		new Thread(consumer1).start();
		new Thread(consumer2).start();
		
		
	}
}

总结

一、因为SynchronousQueue没有存储功能,因此put()和take()会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。

二、使用SunchronousQueue作为工作队列,工作队列本身并不限制待执行的任务的数量。但此时需要限定线程池的最大大小为一个合理的有限值,而不应该是 Integer.MAX_VALUE,否则可能导致线程池中的工作者现成的数量一直增加到系统资源无法承受为止。使用SynchronousQueue的目的就是保证“对于提交的任务,如果有空闲线程,就使用空闲线程来处理;否则新建一个线程来处理任务”

三、take() 和 put() 方法:是阻塞的,会阻塞操作线程。
poll() 和 offer() 方法:是非阻塞的,当操作不能达成的时候会马上返回boolean。即如果调用offer方法,这个时候有线程在等待那么马上返回true,如果没有线程等待则返回false,不会等待消费者到来。

参考并感谢

[1] https://www.jianshu.com/p/b7f7eb2bc778
[2] http://ifeve.com/java-synchronousqueue/
[3] https://juejin.im/post/59f2e50151882546b15bc20d
[4] https://blog.csdn.net/u011518120/article/details/53906484

你可能感兴趣的:(J.U.C源码)