SynchronousQueue—容量为 0 的阻塞队列

1,099 阅读5分钟

0. 起因

之前在 SpringBoot 项目中创建线程池都是使用 org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor 类,线程池的队列容量设置方式为 ThreadPoolTaskExecutor#setQueueCapacity(),支持设置为0。
离开了 SpringBoot 环境,通常使用 java.util.concurrent.ThreadPoolExecutor 来创建线程池,线程池创建方法为:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;

        String name = Objects.toIdentityString(this);
        this.container = SharedThreadContainer.create(name);
    }

可以看到线程池的队列对象必须实现 java.util.concurrent.BlockingQueue 接口,那么要想将线程池的队列容量设置为0,只需要构造构造一个容量为 0 的阻塞队列。然而,你会发现无论是 ArrayBlockingQueue 还是 LinkedBlockingQueue 这些常用的阻塞队列都不支持把容量设置为0。

    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException(); // 容量设置为0直接抛异常
        this.capacity = capacity;
        last = head = new Node<E>(null);
    } 

	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0) // 容量设置为0直接抛异常
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

这时候应该怎么办呢?我们不妨去看看 ThreadPoolTaskExecutor#setQueueCapacity() 是如何构造一个容量为 0 的阻塞队列的。

1. ThreadPoolTaskExecutor#setQueueCapacity() 队列容量为0的实现逻辑

首先是 setQueueCapacity 方法:

    public void setQueueCapacity(int queueCapacity) {
	this.queueCapacity = queueCapacity;
    }

这里只是把线程池队列容量参数赋值给了 ThreadPoolTaskExecutor 的属性,并没有执行具体的队列创建逻辑,那么我们找一下 ThreadPoolTaskExecutor#queueCapacity 这个属性在哪里被使用到。很幸运,只有一处:

    protected ExecutorService initializeExecutor(
			ThreadFactory threadFactory
                        , RejectedExecutionHandler rejectedExecutionHandler) {

		BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
		...
                // 这里省略后续代码
                ...
	}

进入这个 createQueue() 方法:

    protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
	if (queueCapacity > 0) {
	    return new LinkedBlockingQueue<>(queueCapacity);
	}
	else {
	    return new SynchronousQueue<>();
	}
    }

可以看到,当 queueCapacity=0 时,创建的线程池队列是 java.util.concurrent.SynchronousQueue。这个类是 java.util.concurrent 包下的,不依赖 spring。

2. 有意思的 SynchronousQueue

这是 SynchronousQueue 的注释开头:

A blocking queue in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa. A synchronous queue does not have any internal capacity, not even a capacity of one. 

翻译一下大意:

SynchronousQueue 队列的容量为0。任何一个元素的插入操作都必须等待其他线程对这个元素进行移除操作才能生效。  

具体的实现逻辑在内部类 SynchronousQueue.Transferer 中,Transfer接口有两个具体的实现类 TransfererStackTransfererQueue,这两个的主要区别从名称 Stack\Queue 上就能看出来。下面我们主要以实现较为复杂的 TransfererStack 为例来说明。
整个类中最重要的是 transfer() 方法,该方法同时承担 的双重功能,即入栈、出栈都用这个方法

E transfer(E e, boolean timed, long nanos) {

其中, 时 e 是具体的元素数据, 时 e 为 null。以 TransfererStack 为例对 transfer() 方法的源码做一下注解(这里 Doug Lea大佬把存取代码结合在一起,实现的很优雅,但是我们阅读起来会比较困难)。

E transfer(E e, boolean timed, long nanos) {

其中, 时 e 是具体的元素数据, 时 e 为 null。以TransfererStack为例(TransfererQueue相对简单一点)对源码做一下注解(Doug Lea大佬把存取代码结合在一起,实现的很优雅,但是我们阅读起来会比较困难)。

        /**
         * @param e 若不为空,就是put数据,等待消费者取走数据或超时才结束;若为空,就是take数据,直到有数据可取或者超时才返回
         * @param timed 是否有超时设定
         * @param nanos 超时时长
         * 
         * @return 返回值如果为空,代表超时或者中断。否则返回要存或者取到的数据
         **/
        E transfer(E e, boolean timed, long nanos) {
                
            SNode s = null;
            // REQYUEST:取;DATA:存
            int mode = (e == null) ? REQUEST : DATA;
            // 自旋 + CAS
            for (;;) {
                SNode h = head;
                // 1
                // 下面两种情况,当前操作入栈作为头节点
                // 1) 头节点为空
                // 2) 头节点不为空且和当前操作是同一个类型,即都是存或都是取
                // 注:为什么只判断头节点?因为栈中只能有一种类型的节点。除了下面的分支3这种特殊情况:头节点是正在寻找匹配的节点,这时新节点自旋,不允许入栈
                if (h == null || h.mode == mode) { 
                    // 1.1
                    // 有超时时间且时间已到 
                    if (timed && nanos <= 0) {
                        // 1.1.1
                        // 头节点不是null(刚刚有其他线程在操作入栈)且头节点是取消状态(这里取消的含义是头节点已经超时或线程中断,可以看tryCancel()方法)
                        // 则顺便清理一下,然后进入下一次循环,继续清理
                        // 直到进入 1.1.2 状态,返回 null
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);
                        // 1.1.2
                        // 头节点为null,或头节点不为null且不是取消状态,不需要清理/清理已经结束,结束当前操作的循环,返回null
                        else
                            return null;
                    // 1.2
                    // 尝试把当前节点添加到头节点位置
                    // 1)失败,说明头节点被别的线程修改了,那么结束重新进入循环
                    // 2)成功,自选,等待匹配,指导超时或中断
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        // 等待
                        SNode m = awaitFulfill(s, timed, nanos);
                        // 1.2.1 返回的是自身,说明超时或中断了
                        if (m == s) {
                            // 清理自身
                            clean(s);
                            return null;
                        }
                        // 1.2.2 配对成功
                        // 返回之前清理一下其他节点
                        // 1) 如果 head 为 null,说明栈空了,直接结束,不需要处理
                        // 2) 如果当前节点紧跟着栈头,则清理当前节点
                        // 注:为什么只判断到 s 是紧跟着头节点的节点,而不管可能是第三个、第四个节点的情况?因为这是单向链表,无法往前搜索节点。只能留待后续清理。在分支2中
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);    
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                // 2
                // 头节点不是正在主动寻找配对。除了下面的 s=snode(s, e, h, FULFILLING|mode) 这种设置,节点都是这个状态
                // 当前节点准备入栈为头节点,且置为正在主动寻找配对的状态
                } else if (!isFulfilling(h.mode)) {
                    // 2.1 头节点已经取消
                    if (h.isCancelled())
                        // 更换头节点,结束,进入下一次循环
                        casHead(h, h.next);
                    // 2.2 入栈为头节点且设置为主动匹配状态
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        // 一直循环找匹配节点,或中断
                        for (;;) { 
                            // m 是真正的头节点(主动寻找配对节点是个虚假的节点)
                            SNode m = s.next;
                            if (m == null) {
                                // 头节点为null,说明没有等待匹配的节点了,结束循环,进入下一次(?或许有新的节点加入?)
                                casHead(s, null);
                                s = null;
                                break;
                            }
                            SNode mn = m.next;
                            // 依次拿每个节点来匹配 s
                            if (m.tryMatch(s)) {
                                // mn 设为头节点,即丢弃s和m
                                casHead(s, mn);
                                return (E) ((mode == REQUEST) ? m.item : s.item);
                            } else
                                // 匹配不上的节点,需要被剔除(要么被用掉了,要么结束了)
                                s.casNext(m, mn);
                        }
                    }
                // 3
                // 头节点正在寻找匹配节点(即不可以用来匹配当前节点),先执行头节点匹配,完成之后再处理新的当前的新请求 s
                } else {
                    // 取下一个节点
                    SNode m = h.next;
                    if (m == null)
                        // 3.1 下一个节点为null,栈清空,结束,进入下一个循环
                        casHead(h, null);
                    else {
                        // 3.2 尝试和下一个节点匹配
                        SNode mn = m.next;
                        if (m.tryMatch(h))
                            // 匹配成功,两个都出栈;后续循环继续处理 s
                            casHead(h, mn);
                        else
                            // 匹配失败,丢弃,换下一个节点
                            h.casNext(m, mn);
                    }
                }
            }
        }

这里的CAS操作大量使用了一个方法:

UNSAFE.compareAndSwapObject(this, headOffset, h, nh)

一个对象在内存中占有一段连续空间,对象的每个属性各占其中的一小块内存,每个属性的内存地址可以通过 UNSAFE.objectFieldOffset 方法获取。headOffset 就是这样的一个值。
compareAndSwapObject 这个方法作用是:

this 对象在内存中偏移 headOffset 处存储的属性,如果是 h,就修改为 nh。

源码比较复杂,主要流程可以总结如下:

SynchronousStack流程.drawio.png