SynchronousQueue

阻塞队列-SynchronousQueue

最近在学习并发编程里面的阻塞队列部分,给大家介绍第一个阻塞队列SynchronousQueue,下面是本文的目录:
一 SynchronousQueue介绍和使用场景
二 SynchronousQueue示例代码
三 SynchronousQueue的源码分析

原创不易,如果转载请注明链接:https://blog.youkuaiyun.com/qq_38305457/article/details/94740689


我不能保证写的每个地方都是对的,但是至少能保证不复制,不黏贴,保证每一句话,每一行代码都经过了认真的推敲,仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术,对于生活的态度。
我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正的改变世界。
面对压力,我可以挑灯夜战,不眠不休;面对困难,我愿意迎难而上,永不退缩。 其实我想说的是:我只是一个程序员,这就是我现在纯粹人生的全部。


一 SynchronousQueue介绍和使用场景

SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。

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

SynchronousQueue适用于生产者生产能力和消费者消费能力接近的场景。如果生产数据过大,而消费者消费不及时,这时效率反而降低。它模拟的功能类似于生活中一手交钱一手交货这种情形,像那种货到付款或者先付款后发货模型不适合使用SynchronousQueue。首先要知道SynchronousQueue没有容纳元素的能力,即它的isEmpty()方法总是返回true,但是给人的感觉却像是只能容纳一个元素。

二 SynchronousQueue示例代码

package com.example.demo;
import java.util.Random;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class SynchronousQueueTest {
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
        Worktwo worktwo = new Worktwo(queue);
        Workone workone = new Workone(queue);
        Thread t1= new Thread(worktwo);
        Thread t2= new Thread(workone);
        t1.start();
        t2.start();
    }

}
/**
 * 模拟生产者
 * @author wfd
 * 2019-07-10 20:36
 */
class Workone implements Runnable{
    SynchronousQueue<Integer> queue = null;

    public Workone(){

    }

    public Workone(SynchronousQueue<Integer> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        int rand = new Random().nextInt(1000);
        System.out.println(String.format("模拟生产者:%d",rand));
        try{
            TimeUnit.SECONDS.sleep(3);
            queue.put(rand);
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println(queue.isEmpty());
    }

}
/**
 * 模拟消费者
 * @author wfd
 * 2019-07-10 20:36
 */
class Worktwo implements Runnable{
    SynchronousQueue<Integer> queue = null;

    public Worktwo(){

    }

    public Worktwo(SynchronousQueue<Integer> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("消费者已经准备好接受元素了...");
        try{
            System.out.println(String.format("消费一个元素:%d", queue.take()));
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("------------------------------------------");
    }
}

下面再介绍一下SynchronousQueue的一些方法
put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。

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();
        }
    }

offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。

public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        return transferer.transfer(e, true, 0) != null;
    }

offer(2000, TimeUnit.SECONDS) 往queue里放一个element但是等待指定的时间后才返回,返回的逻辑和offer()方法一样。

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();
    }

take() 取出并且remove掉queue里的element(认为是在queue里的。。。),取不到东西他会一直等。

public E take() throws InterruptedException {
        E e = transferer.transfer(null, false, 0);
        if (e != null)
            return e;
        Thread.interrupted();
        throw new InterruptedException();
    }

poll() 取出并且remove掉queue里的element(认为是在queue里的。。。),只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。

public E poll() {
        return transferer.transfer(null, true, 0);
    }

poll(2000, TimeUnit.SECONDS) 等待指定的时间然后取出并且remove掉queue里的element,其实就是再等其他的thread来往里塞。

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();
    }

SynchronousQueue Empty()和size()永远是true。

public boolean isEmpty() {
        return true;
    }
    public int size() {
        return 0;
    }

三 SynchronousQueue的源码分析

// CPU的数量
static final int NCPUS = Runtime.getRuntime().availableProcessors();
// 有超时的情况自旋多少次,当CPU数量小于2的时候不自旋
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
// 没有超时的情况自旋多少次
static final int maxUntimedSpins = maxTimedSpins * 16;
// 针对有超时的情况,自旋了多少次后,如果剩余时间大于1000纳秒就使用带时间的LockSupport.parkNanos()这个方法
static final long spinForTimeoutThreshold = 1000L;
// 传输器,即两个线程交换元素使用的东西
private transient volatile Transferer<E> transferer;

通过属性我们可以Get到两个点:

(1)这个阻塞队列里面是会自旋的;

(2)它使用了一个叫做transferer的东西来交换元素;
主要内部类

// Transferer抽象类,主要定义了一个transfer方法用来传输元素
abstract static class Transferer<E> {
    abstract E transfer(E e, boolean timed, long nanos);
}
// 以栈方式实现的Transferer
static final class TransferStack<E> extends Transferer<E> {
    // 栈中节点的几种类型:
    // 1. 消费者(请求数据的)
    static final int REQUEST    = 0;
    // 2. 生产者(提供数据的)
    static final int DATA       = 1;
    // 3. 二者正在撮合中
    static final int FULFILLING = 2;

    // 栈中的节点
    static final class SNode {
        // 下一个节点
        volatile SNode next;        // next node in stack
        // 匹配者
        volatile SNode match;       // the node matched to this
        // 等待着的线程
        volatile Thread waiter;     // to control park/unpark
        // 元素
        Object item;                // data; or null for REQUESTs
        // 模式,也就是节点的类型,是消费者,是生产者,还是正在撮合中
        int mode;
    }
    // 栈的头节点
    volatile SNode head;
}
// 以队列方式实现的Transferer
static final class TransferQueue<E> extends Transferer<E> {
    // 队列中的节点
    static final class QNode {
        // 下一个节点
        volatile QNode next;          // next node in queue
        // 存储的元素
        volatile Object item;         // CAS'ed to or from null
        // 等待着的线程
        volatile Thread waiter;       // to control park/unpark
        // 是否是数据节点
        final boolean isData;
    }

    // 队列的头节点
    transient volatile QNode head;
    // 队列的尾节点
    transient volatile QNode tail;
}
(1)定义了一个抽象类Transferer,里面定义了一个传输元素的方法;

(2)有两种传输元素的方法,一种是栈,一种是队列;

(3)栈的特点是后进先出,队列的特点是先进行出;

(4)栈只需要保存一个头节点就可以了,因为存取元素都是操作头节点;

(5)队列需要保存一个头节点一个尾节点,因为存元素操作尾节点,取元素操作头节点;

(6)每个节点中保存着存储的元素、等待着的线程,以及下一个节点;

(7)栈和队列两种方式有什么不同呢?请看下面的分析。
主要构造方法

public SynchronousQueue() {
    // 默认非公平模式
    this(false);
}

public SynchronousQueue(boolean fair) {
    // 如果是公平模式就使用队列,如果是非公平模式就使用栈
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
1)默认使用非公平模式,也就是栈结构;

(2)公平模式使用队列,非公平模式使用栈;
入队

我们这里主要介绍以栈方式实现的传输模式,以put(E e)方法为例。

public void put(E e) throws InterruptedException {
    // 元素不可为空
    if (e == null) throw new NullPointerException();
    // 直接调用传输器的transfer()方法
    // 三个参数分别是:传输的元素,是否需要超时,超时的时间
    if (transferer.transfer(e, false, 0) == null) {
        // 如果传输失败,直接让线程中断并抛出中断异常
        Thread.interrupted();
        throw new InterruptedException();
    }
}

调用transferer的transfer()方法,传入元素e,说明是生产者

出队

我们这里主要介绍以栈方式实现的传输模式,以take()方法为例。

public E take() throws InterruptedException {
    // 直接调用传输器的transfer()方法
    // 三个参数分别是:null,是否需要超时,超时的时间
    // 第一个参数为null表示是消费者,要取元素
    E e = transferer.transfer(null, false, 0);
    // 如果取到了元素就返回
    if (e != null)
        return e;
    // 否则让线程中断并抛出中断异常
    Thread.interrupted();
    throw new InterruptedException();
}
调用transferer的transfer()方法,传入null,说明是消费者。

transfer()方法

transfer()方法同时实现了取元素和放元素的功能,下面我再来看看这个transfer()方法里究竟干了什么。

// TransferStack.transfer()方法
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
    // 根据e是否为null决定是生产者还是消费者
    int mode = (e == null) ? REQUEST : DATA;
    // 自旋+CAS,熟悉的套路,熟悉的味道
    for (;;) {
        // 栈顶元素
        SNode h = head;
        // 栈顶没有元素,或者栈顶元素跟当前元素是一个模式的
        // 也就是都是生产者节点或者都是消费者节点
        if (h == null || h.mode == mode) {  // empty or same-mode
            // 如果有超时而且已到期
            if (timed && nanos <= 0) {      // can't wait
                // 如果头节点不为空且是取消状态
                if (h != null && h.isCancelled())
                    // 就把头节点弹出,并进入下一次循环
                    casHead(h, h.next);     // pop cancelled node
                else
                    // 否则,直接返回null(超时返回null)
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                // 入栈成功(因为是模式相同的,所以只能入栈)
                // 调用awaitFulfill()方法自旋+阻塞当前入栈的线程并等待被匹配到
                SNode m = awaitFulfill(s, timed, nanos);
                // 如果m等于s,说明取消了,那么就把它清除掉,并返回null
                if (m == s) {               // wait was cancelled
                    clean(s);
                    // 被取消了返回null
                    return null;
                }

                // 到这里说明匹配到元素了
                // 因为从awaitFulfill()里面出来要不被取消了要不就匹配到了

                // 如果头节点不为空,并且头节点的下一个节点是s
                // 就把头节点换成s的下一个节点
                // 也就是把h和s都弹出了
                // 也就是把栈顶两个元素都弹出了
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                // 根据当前节点的模式判断返回m还是s中的值
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            // 到这里说明头节点和当前节点模式不一样
            // 如果头节点不是正在撮合中

            // 如果头节点已经取消了,就把它弹出栈
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                // 头节点没有在撮合中,就让当前节点先入队,再让他们尝试匹配
                // 且s成为了新的头节点,它的状态是正在撮合中
                for (;;) { // loop until matched or waiters disappear
                    SNode m = s.next;       // m is s's match
                    // 如果m为null,说明除了s节点外的节点都被其它线程先一步撮合掉了
                    // 就清空栈并跳出内部循环,到外部循环再重新入栈判断
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                    // 如果m和s尝试撮合成功,就弹出栈顶的两个元素m和s
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        // 返回撮合结果
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        // 尝试撮合失败,说明m已经先一步被其它线程撮合了
                        // 就协助清除它
                        s.casNext(m, mn);   // help unlink
                }
            }
        } else {                            // help a fulfiller
            // 到这里说明当前节点和头节点模式不一样
            // 且头节点是正在撮合中

            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                // 如果m为null,说明m已经被其它线程先一步撮合了
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                // 协助匹配,如果m和s尝试撮合成功,就弹出栈顶的两个元素m和s
                if (m.tryMatch(h))          // help match
                    // 将栈顶的两个元素弹出后,再让s重新入栈
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    // 尝试撮合失败,说明m已经先一步被其它线程撮合了
                    // 就协助清除它
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

// 三个参数:需要等待的节点,是否需要超时,超时时间
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 ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        // 当前线程中断了,尝试清除s
        if (w.isInterrupted())
            s.tryCancel();

        // 检查s是否匹配到了元素m(有可能是其它线程的m匹配到当前线程的s)
        SNode m = s.match;
        // 如果匹配到了,直接返回m
        if (m != null)
            return m;

        // 如果需要超时
        if (timed) {
            // 检查超时时间如果小于0了,尝试清除s
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel();
                continue;
            }
        }
        if (spins > 0)
            // 如果还有自旋次数,自旋次数减一,并进入下一次自旋
            spins = shouldSpin(s) ? (spins-1) : 0;

        // 后面的elseif都是自旋次数没有了
        else if (s.waiter == null)
            // 如果s的waiter为null,把当前线程注入进去,并进入下一次自旋
            s.waiter = w; // establish waiter so can park next iter
        else if (!timed)
            // 如果不允许超时,直接阻塞,并等待被其它线程唤醒,唤醒后继续自旋并查看是否匹配到了元素
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            // 如果允许超时且还有剩余时间,就阻塞相应时间
            LockSupport.parkNanos(this, nanos);
    }
}

    // SNode里面的方向,调用者m是s的下一个节点
    // 这时候m节点的线程应该是阻塞状态的
    boolean tryMatch(SNode s) {
        // 如果m还没有匹配者,就把s作为它的匹配者
        if (match == null &&
            UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
            Thread w = waiter;
            if (w != null) {    // waiters need at most one unpark
                waiter = null;
                // 唤醒m中的线程,两者匹配完毕
                LockSupport.unpark(w);
            }
            // 匹配到了返回true
            return true;
        }
        // 可能其它线程先一步匹配了m,返回其是否是s
        return match == s;
    }

整个逻辑比较复杂,这里为了简单起见,屏蔽掉多线程处理的细节,只描述正常业务场景下的逻辑:

(1)如果栈中没有元素,或者栈顶元素跟将要入栈的元素模式一样,就入栈;

(2)入栈后自旋等待一会看有没有其它线程匹配到它,自旋完了还没匹配到元素就阻塞等待;

(3)阻塞等待被唤醒了说明其它线程匹配到了当前的元素,就返回匹配到的元素;

(4)如果两者模式不一样,且头节点没有在匹配中,就拿当前节点跟它匹配,匹配成功了就返回匹配到的元素;

(5)如果两者模式不一样,且头节点正在匹配中,当前线程就协助去匹配,匹配完成了再让当前节点重新入栈重新匹配;

上面的源码分析都是基于Stack的方式来分析的,那么队列是怎么动作的呢?很简单哦,测试用例中的false改成true就可以了,这就交给你了。

python+opencv简谱识别音频生成系统源码含GUI界面+详细运行教程+数据 一、项目简介 提取简谱中的音乐信息,依据识别到的信息生成midi文件。 Extract music information from musical scores and generate a midi file according to it. 二、项目运行环境 python=3.11.1 第三方库依赖 opencv-python=4.7.0.68 numpy=1.24.1 可以使用命令 pip install -r requirements.txt 来安装所需的第三方库。 三、项目运行步骤 3.1 命令行运行 运行main.py。 输入简谱路径:支持图片或文件夹,相对路径或绝对路径都可以。 输入简谱主音:它通常在第一页的左上角“1=”之后。 输入简谱速度:即每分钟拍数,同在左上角。 选择是否输出程序中间提示信息:请输入Y或N(不区分大小写,下同)。 选择匹配精度:请输入L或M或H,对应低/中/高精度,一般而言输入L即可。 选择使用的线程数:一般与CPU核数相同即可。虽然python的线程不是真正的多线程,但仍能起到加速作用。 估算字符上下间距:这与简谱中符号的密集程度有关,一般来说纵向符号越稀疏,这个值需要设置得越大,范围通常在1.0-2.5。 二值化算法:使用全局阈值则跳过该选项即可,或者也可输入OTSU、采用大津二值化算法。 设置全局阈值:如果上面选择全局阈值则需要手动设置全局阈值,对于.\test.txt中所提样例,使用全局阈值并在后面设置为160即可。 手动调整中间结果:若输入Y/y,则在识别简谱后会暂停代码,并生成一份txt文件,在其中展示识别结果,此时用户可以通过修改这份txt文件来更正识别结果。 如果选择文件夹的话,还可以选择所选文件夹中不需要识别的文件以排除干扰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值