JUC(八)-阻塞队列-SynchronousQueue部分代码详解、测试使用

阻塞队列 SynchronousQueue 详解

一、SynchronousQueue介绍

  1. 这个阻塞队列和其他的阻塞队列有很大的区别 , 队列的概念一般是要进行存储数据的
    而这个阻塞队列并不会存储数据 , 这个队列会把生产者和消费者放到队列中让他们去配对

  2. 当存储了一个生产者到SynchronousQueue队列之后,生产者会阻塞(例如poll(time,unit))或者不允许阻塞直接进行匹配例如poll方法
    生产者最终会有以下几种结果
    <1> 如果生产者在阻塞期间有消费者来匹配 , 生产者就会将绑定的消息交给消费者
    <2> 生产者等待到阻塞结束、或者生产者不允许阻塞(poll方法) , 那么当前生产者直接失败
    <3> 生产者在阻塞期间,被线程中断了,那么同样的这个生产者直接失败
    同理,消费者和生产者一样 , 当消费者把自己扔到SynchronousQueue队列中时要去匹配生产者,其效果和上边生产者的三种状态类似

  3. 从上边2可以得出结论
    <1.> 生产者和消费者不会像之前的阻塞队列那样 会向底层封装的存储数据的容器打交道,而是生产者和消费者直接进行通信,不经过阻塞队列的容器
    <2.> 相当于是生产者直接把数据给到消费者

  4. 探究生产者调用不同方法的区别
    生产者:
    <1.> offer()方法 : 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
    如果没有消费者正在等待生产者的消息 , 那么就直接返回,结束方法
    <2.> offer(time,unit): 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
    如果没有消费者正在等待生产者的消息 , 那么该生产者就阻塞time时间 , 之后如果还没有那么直接返回,结束方法
    ❤️.> put() : 生产者在放到SynchronousQueue的同时,如果有消费者正在等待生产者的消息,那么就直接配对
    如果没有消费者正在等待生产者的消息 , 那么该生产者就死等
    消费者: poll()、poll(time,unit)、take() 方法,和上边生产者道理一致

  5. 测试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 指针
      • image.png
    • TransferQueue : 队列结构
      • 队列结构存在 head和tail 并且默认指向的是一个伪节点
      • image.png
// 默认就是非公平实现即 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;
        }
    }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值