`牵手容器` SynchronousQueue

350 阅读12分钟

1、简介

SynchronousQueue 是一个容器大小为0但存储元素的阻塞容器,存储结构为链表;那这里为什么说是牵手容器呢?

这个容器就一个牵桥搭线的地方,为广大的单身同胞元素进行成对匹配,牵手成功后,就双宿双飞了,但是也是有一定规则的

  1. 匹配只能从排队固定一端进行匹配,排队也必须从固定一端进行
  2. 元素的操作分为读取、存放;读取-读取 存放-存放为同性, 读取-存放, 存放-读取为异性
  3. 同性元素进行排队,异性元素进行匹配,匹配成功,则一起牵手离开
  4. 当多个异性元素来临时,队端的那个元素去领走心中满意的一个异性;若队列中没有,则这些异性元素排队,否则重复此过程
  5. 排队只是一个宽泛的表示:很多同性元素在等待或者暂时没有异性元素,不等待孤独的走了、等一会走了、等到匹配成功、未成功却由于外力因素被叫走,我都称为排队

这个比喻也基本上说出了这个容器的所有操作;此容器的核心也就是读取、存储;其它容器操作基本是固定的,没有太多意义的;Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

相比大家心里已经有了大概了,让我从代码的角度再来确认以下这个过程

2、SynchronousQueue 源码

内部使用了Transfer对象来代理实现,因为分为公平模式、非公平模式,也可说使用了策略模式,其本身代码还是十分简单的

2.1 构造器

使用存在两种模式,公平模式、非公平模式,默认使用非公平模式

    public SynchronousQueue() {
        this(false);
    }

    public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }

2.2 存取

存取方法使用Transferer对象来完成,也就是代理模式

    private transient volatile Transferer<E> transferer;

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        if (transferer.transfer(e, false, 0) == null) {
            Thread.interrupted();
            throw new InterruptedException();
        }
    }
    
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (e == null) throw new NullPointerException();
        if (transferer.transfer(e, true, unit.toNanos(timeout)) != null)
            return true;
        if (!Thread.interrupted())
            return false;
        throw new InterruptedException();
    }

   public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        return transferer.transfer(e, true, 0) != null;
    }
    
    public E take() throws InterruptedException {
        E e = transferer.transfer(null, false, 0);
        if (e != null)
            return e;
        Thread.interrupted();
        throw new InterruptedException();
    }
    
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E e = transferer.transfer(null, true, unit.toNanos(timeout));
        if (e != null || !Thread.interrupted())
            return e;
        throw new InterruptedException();
    }
    
    public E poll() {
        return transferer.transfer(null, true, 0);
    }

2.3 其它操作

容器为空,大小为0,不包含任何元素

    public boolean isEmpty() {
        return true;
    }
    
    public int size() {
        return 0;
    }
    
    public boolean contains(Object o) {
        return false;
    }

无删除清理动作

    public void clear() {
    }
    
    public boolean remove(Object o) {
        return false;
    }

3、非公平模式 TransferStack

采用先进后出的模式;也就是后来排队的有优先牵手的机会

3.1 数据节点 SNode

        static final class SNode {
            volatile SNode next;    
            volatile SNode match;     
            volatile Thread waiter;   
            Object item;    
            int mode;

            SNode(Object item) {
                this.item = item;
            }

            boolean casNext(SNode cmp, SNode val) {
                return cmp == next &&
                    U.compareAndSwapObject(this, NEXT, cmp, val);
            }

            boolean tryMatch(SNode s) {
                if (match == null &&
                    U.compareAndSwapObject(this, MATCH, null, s)) {
                    Thread w = waiter;
                    if (w != null) {
                        waiter = null;
                        LockSupport.unpark(w);
                    }
                    return true;
                }
                return match == s;
            }

            void tryCancel() {
                U.compareAndSwapObject(this, MATCH, null, this);
            }

            boolean isCancelled() {
                return match == this;
            }

            private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
            private static final long MATCH;
            private static final long NEXT;

            static {
                try {
                    MATCH = U.objectFieldOffset
                        (SNode.class.getDeclaredField("match"));
                    NEXT = U.objectFieldOffset
                        (SNode.class.getDeclaredField("next"));
                } catch (ReflectiveOperationException e) {
                    throw new Error(e);
                }
            }
        }

代码中可以得到信息:

  1. 单链表结构,包含下个节点信息,牵手成功对象节点信息、节点性别、节点所在线程对象
  2. 对next、match节点进行原子操作

3.1.1 tryMatch方法

boolean tryMatch(SNode s) {
                if (match == null &&
                    U.compareAndSwapObject(this, MATCH, null, s)) {
                    Thread w = waiter;
                    if (w != null) {
                        waiter = null;
                        LockSupport.unpark(w);
                    }
                    return true;
                }
                return match == s;
            }

如果当前节点未匹配到数据,则原子操作至当前匹配为传参,并释放唤醒节点线程;否则当前节点已经匹配成功且被唤醒,则直接返回是否匹配的为传入参数,这个时候基本返回false,也就是传入参数节点需要重新去匹配

3.1.2 tryCancel方法

            void tryCancel() {
                U.compareAndSwapObject(this, MATCH, null, this);
            }

match指针指向自己;这个的调用场景,是存在超时等待,结束后,自我匹配,然后独自离开

3.2 头指针

        volatile SNode head;

        boolean casHead(SNode h, SNode nh) {
            return h == head &&
                U.compareAndSwapObject(this, HEAD, h, nh);
        }

头指针实现了原子操作

3.3 节点状态

        static final int REQUEST    = 0;
        
        static final int DATA       = 1;
  
        static final int FULFILLING = 2;

        static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }

有三种状态:读状态、存状态、临时读/写数据状态(为了线程安全的一个临时态)

3.4 shouldSpin方法

        boolean shouldSpin(SNode s) {
            SNode h = head;
            return (h == s || h == null || isFulfilling(h.mode));
        }

传参节点为头节点、头节点为空、或者节点状态为读取状态;也就是说如果头数据被置为一个临时态的数据,不需要自旋

3.5 awaitFulfill方法

        SNode awaitFulfill(SNode s, boolean timed, long nanos) {
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Thread w = Thread.currentThread();
            int spins = shouldSpin(s)
                ? (timed ? MAX_TIMED_SPINS : MAX_UNTIMED_SPINS)
                : 0;
            for (;;) {
                if (w.isInterrupted())
                    s.tryCancel();
                SNode m = s.match;
                if (m != null)
                    return m;
                if (timed) {
                    nanos = deadline - System.nanoTime();
                    if (nanos <= 0L) {
                        s.tryCancel();
                        continue;
                    }
                }
                if (spins > 0)
                    spins = shouldSpin(s) ? (spins - 1) : 0;
                else if (s.waiter == null)
                    s.waiter = w; // establish waiter so can park next iter
                else if (!timed)
                    LockSupport.park(this);
                else if (nanos > SPIN_FOR_TIMEOUT_THRESHOLD)
                    LockSupport.parkNanos(this, nanos);
            }
        }

大致流程

  1. 计算等待结束时间、自旋次数
  2. for循环进行自旋
  3. 如果s节点线程被打断,则cas操作进行自我牵手
  4. 如果匹配数据不为空,返回匹配数据,可能是自我牵手离场,也可能是匹配成功,成对离场
  5. 如果是超时时长结束,则cas操作自我牵手,继续下次循环
  6. 自旋次数大于0时,减1,继续下次循环
  7. 如果节点线程对象为空,则置为当前线程
  8. 如果是无限等待策略,则线程挂起
  9. 5中超时时长大于某个值时,则超时时长挂起线程,否则等待5计算跳出循环结束

3.6 clean方法

        void clean(SNode s) {
            s.item = null; 
            s.waiter = null;

            SNode past = s.next;
            if (past != null && past.isCancelled())
                past = past.next;

            SNode p;
            while ((p = head) != null && p != past && p.isCancelled())
                casHead(p, p.next);

            while (p != null && p != past) {
                SNode n = p.next;
                if (n != null && n.isCancelled())
                    p.casNext(n, n.next);
                else
                    p = n;
            }
        }

从代码中可以看出,做了两件事

  1. 把现有节点链表中,找到第一个不为空,且没有取消的节点做为头节点
  2. 从当前节点开始,去掉为空节点或者取消节点

3.7 核心方法 transfer

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

            SNode s = null;
            int mode = (e == null) ? REQUEST : DATA;

            for (;;) {
                SNode h = head;
                if (h == null || h.mode == mode) { 
                    if (timed && nanos <= 0L) { 
                        if (h != null && h.isCancelled())
                            casHead(h, h.next);
                        else
                            return null;
                    } else if (casHead(h, s = snode(s, e, h, mode))) {
                        SNode m = awaitFulfill(s, timed, nanos);
                        if (m == s) {  
                            clean(s);
                            return null;
                        }
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);  
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    }
                } else if (!isFulfilling(h.mode)) { 
                    if (h.isCancelled())         
                        casHead(h, h.next);   
                    else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                        for (;;) {
                            SNode m = s.next;      
                            if (m == null) {    
                                casHead(s, null); 
                                s = null;  
                                break;  
                            }
                            SNode mn = m.next;
                            if (m.tryMatch(s)) {
                                casHead(s, mn); 
                                return (E) ((mode == REQUEST) ? m.item : s.item);
                            } else                
                                s.casNext(m, mn);  
                        }
                    }
                } else {                           
                    SNode m = h.next;              
                    if (m == null)                  
                        casHead(h, null);          
                    else {
                        SNode mn = m.next;
                        if (m.tryMatch(h))         
                            casHead(h, mn);        
                        else                    
                            h.casNext(m, mn);    
                    }
                }
            }
        }

首先对参数进行说明:

  • E e 数据,如果请求则为null,如果插入则为要插入数据
  • boolean timed,超时规则,如果false,则无限等待,否则有限时间内等待
  • long nanos,有限等待时长,若小于等于0表示不等待

分为以下情况处理:整个处理是一个for自旋操作,保证cas操作的失败(数据已经被其它线程操作破坏)时,重头处理

  1. 栈中无数据,或者栈头数据类别和当前数据模式一致
  • 不等待时,检查栈头是否为有效,无效则重置头,重新for循环处理,有效则返回null
  • 生成节点,并且cas操作置为栈头:失败,则表示栈头发生变化,重新for自旋,否则使用方法awaitFulfill 等待查找匹配对象;匹配对象等于自己,则使用clean清理数据,并返回null;cas操作头节点为当前节点的下个节点,返回数据
  1. 栈头数据类别和当前数据不一致,且栈头状态不是临时态
  • 首先对栈头状态判断,无效则往后cas置换;
  • 生成临时读/写状态节点(为啥是临时读写呢,因为它和栈头节点可以牵手成功,根根不用入栈的),并cas操作置为头节点;cas失败,则重新for自旋,否则增加一个内部for自旋,a)判断状态当前节点后无节点,跳出内循环,从外循环重新开始,b) 尝试和下个节点匹配,匹配成功,则返回,c) 失败,则把节点重置为下下个节点,重新内循环(原理:栈里等待全部与新节点异性,总会能找到一个,如果栈内异性节点都没有了,按照a跳出内循环)
  1. 栈头数据状态为临时态
  • 说明栈头节点的下一个节点已经有匹配对象,这时,则需要帮助它们牵手离开;如果栈头的下个节点为空,则栈头cas置换为null;否则cas尝试牵手,成功则重置栈头为栈头下下个节点,否则栈头的下个节点已经有匹配的了,则把栈头next节点置为栈头next-next节点

4、公平模式 TransferQueue

和非公平类似,但又有不同,不同点:从链表尾部插入节点等待,从链表头取出数据匹配

4.1 节点QNode

    static final class QNode {
            volatile QNode next;      
            volatile Object item;       
            volatile Thread waiter;     
            final boolean isData;

            QNode(Object item, boolean isData) {
                this.item = item;
                this.isData = isData;
            }

            boolean casNext(QNode cmp, QNode val) {
                return next == cmp &&
                    U.compareAndSwapObject(this, NEXT, cmp, val);
            }

            boolean casItem(Object cmp, Object val) {
                return item == cmp &&
                    U.compareAndSwapObject(this, ITEM, cmp, val);
            }

            void tryCancel(Object cmp) {
                U.compareAndSwapObject(this, ITEM, cmp, this);
            }

            boolean isCancelled() {
                return item == this;
            }

            boolean isOffList() {
                return next == this;
            }

            private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
            private static final long ITEM;
            private static final long NEXT;

            static {
                try {
                    ITEM = U.objectFieldOffset
                        (QNode.class.getDeclaredField("item"));
                    NEXT = U.objectFieldOffset
                        (QNode.class.getDeclaredField("next"));
                } catch (ReflectiveOperationException e) {
                    throw new Error(e);
                }
            }
        }

实现了cas原子操作,使用item保存当前数据,和匹配数据,isData标志读/写数据

4.2 成员变量

        transient volatile QNode head;
        transient volatile QNode tail;

        transient volatile QNode cleanMe;

头节点、尾节点,待清理节点

4.3 awaitFulfill方法

内容和非公平模式基本一样了,就不分析了;但由于数据结构不同,在判断item是否为匹配数据变化为:如果item数据不为节点的数据,则为匹配数据

                Object x = s.item;
                if (x != e)
                    return x;

4.4 clean方法

void clean(QNode pred, QNode s) {
            s.waiter = null; 
            while (pred.next == s) { 
                QNode h = head;
                QNode hn = h.next;
                if (hn != null && hn.isCancelled()) {
                    advanceHead(h, hn);
                    continue;
                }
                QNode t = tail; 
                if (t == h)
                    return;
                QNode tn = t.next;
                if (t != tail)
                    continue;
                if (tn != null) {
                    advanceTail(t, tn);
                    continue;
                }
                if (s != t) { 
                    QNode sn = s.next;
                    if (sn == s || pred.casNext(s, sn))
                        return;
                }
                QNode dp = cleanMe;
                if (dp != null) {
                    QNode d = dp.next;
                    QNode dn;
                    if (d == null ||   
                        d == dp ||            
                        !d.isCancelled() ||     
                        (d != t &&               
                         (dn = d.next) != null &&  
                         dn != d &&              
                         dp.casNext(d, dn)))    
                        casCleanMe(dp, null);
                    if (dp == pred)
                        return;    
                } else if (casCleanMe(null, pred))
                    return;     
            }
        }
  • 如果已经清理成功,则不做任何其它处理
  • 检查队列头,无效,则找到一个有效的队列头(不为空,且未取消)重新开始;
  • 如果头尾相同,结束
  • 检查尾部节点使其有效;无效则重新开始
  • 当前待清理节点为空,则把传入参数作为待清理节点,重新开始
  • 当前待清理节点不为空,则进行清理操作,如果操作成功,则把待清理节点置空,如果待清理节点,确实为清理节点,则返回,否则重新开始

4.5 核心方法 transfer

E transfer(E e, boolean timed, long nanos) {
            QNode s = null; 
            boolean isData = (e != null);

            for (;;) {
                QNode t = tail;
                QNode h = head;
                if (t == null || h == null)        
                    continue;     

                if (h == t || t.isData == isData) {
                    QNode tn = t.next;
                    if (t != tail) 
                        continue;
                    if (tn != null) { 
                        advanceTail(t, tn);
                        continue;
                    }
                    if (timed && nanos <= 0L)
                        return null;
                    if (s == null)
                        s = new QNode(e, isData);
                    if (!t.casNext(null, s)) 
                        continue;

                    advanceTail(t, s);  
                    Object x = awaitFulfill(s, e, timed, nanos);
                    if (x == s) {   
                        clean(t, s);
                        return null;
                    }

                    if (!s.isOffList()) {  
                        advanceHead(t, s);   
                        if (x != null)         
                            s.item = s;
                        s.waiter = null;
                    }
                    return (x != null) ? (E)x : e;

                } else {  
                    QNode m = h.next; 
                    if (t != tail || m == null || h != head)
                        continue; 

                    Object x = m.item;
                    if (isData == (x != null) ||
                        x == m ||           
                        !m.casItem(x, e)) {   
                        advanceHead(h, m);   
                        continue;
                    }

                    advanceHead(h, m);    
                    LockSupport.unpark(m.waiter);
                    return (x != null) ? (E)x : e;
                }
            }
        }

for循环自旋,流程有两种情况:

  • 存在异性节点
  1. 检查数据节点是否发生变化,发生变换重新for循环
  2. 检查数据状态和匹配数据是否一致,匹配数据是否为取消数据,重置节点数据失败,则加入对头,重新处理
  3. 这是匹配数据是有效的,唤醒匹配的线程,返回数据
  • 仅仅存在同性节点
  1. 检查尾节点状态,有效且真的是尾节点
  2. 如果不等待,则返回null
  3. 生成新节点,并连接为队尾的下个节点
  4. 移动队尾节点为新节点
  5. awaitFulfill 等待返回匹配数据
  6. 匹配节点为自身,则清理当前节点,返回null
  7. 如果队列中数据存在,则找到下个可能有效的头部
  8. 返回数据

5、结语

这是一个巧妙的算法实现,也可以说是一种“生产者-消费者模式”,只不过是针对线程任务的容器;只不过生产的东西必须等待消费,或者自行离开;使用自旋+原子操作+线程挂起唤醒,减少线程挂起唤醒的资源消耗;非公平模式相对于公平模式,匹配均在栈头,增加线程同步的复杂度;暂时只有线程池中,创建仅仅有缓存线程时,有使用场景,为了有任务就可创建线程来执行;

自旋+原子操作,保证同步时,总是在进行cas操作时,进行状态的重新检查;以保证操作的基础是正确的,不正确则需要从头开始了

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!