多线程案例-阻塞式队列

1.什么是阻塞队列

阻塞队列是一种特殊的队列,在"先进先出"的原则下又引入了"阻塞"功能

阻塞队列能是一种线程安全的数据结构,具有以下特性:

当队列满的时候,继续入队列就会阻塞,直到其它线程从队列中取走元素
当队列空的时候,继续出队列就会阻塞,直到其它队列向队列中插入元素

阻塞式队列的典型应用场景是"生产者消费者模型"

2.生产者消费者模型

生产者消费者模型通过一个容器来解决生产者和消费者的强耦合问题,生产者与消费者之间不直接通讯,通过阻塞队列来通讯,生产数据后放入阻塞队列,消费者可以直接从阻塞队列获取数据

生产者消费者模型能带来两个非常重要的好处:

1.阻塞队列能使生产者和消费者之间解耦
2.阻塞队列起到缓冲的作用,平衡了生产者消费者的处理能力

1.开发中典型的场景:服务器之间的相互调用

当客户端程序向A服务器发起一个请求后,A服务器将请求转发给B服务器处理,然后B服务器处理完成后将结果返回,此时可以视为:A调用了B

这种场景下,AB两个服务器的耦合程度是比较高的!!如果B服务器出现问题,也会引起A的bug,不仅如此,如果A再需要调用C服务器,还需要修改A的代码,非常麻烦..

针对这种场景使用生产者消费者模型,能有效降低耦合

此时,AB之间的耦合就降低很多了,AB都只知道队列的存在,A的代码不与B相关,B的代码也不与A相关,AB任何一方出现问题不会影响到另一方

2.服务器开发中,用户发送的请求的数量是不可控的,如果没有充分的准备并且请求量超过了服务器的承受范围,服务器有可能直接被大量的请求冲垮.例如"秒杀"这种场景,服务器就会同一时刻受到大量请求,这个时候可以把这些请求放到阻塞队列中,让线程慢慢处理,能有效防止服务器被大量的请求冲垮

在Java标准中内置有阻塞队列BlockingQueue,是一个接口,实现类是LinkedBlockingQueue,put方法是入队列,take是出队列.这两个方法具有阻塞特性

下面使用标准库中的阻塞队列实现生产者消费者模型

public class ThreadDemo1 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        //消费者
        Thread customer = new Thread(()->{
            while(true){
                try {
                    int value = queue.take();
                    System.out.println("消费元素: "+value);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"消费者");
        customer.start();
        //生产者
        Thread producer = new Thread(()->{
            Random random = new Random();
            while(true){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                int num = random.nextInt(1000);
                System.out.println("生产元素: "+num);
                try {
                    queue.put(num);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"生产者");
        producer.start();
        try {
            customer.join();
            producer.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

结果

我们可以发现,生产和消费是成对出现的,程序开始运行,因为在生产者线程中让线程休眠500ms后再执行,此时阻塞队列中为空,而消费者线程要再阻塞队列中使用take方法取元素,就会陷入阻塞状态,等到阻塞队列中有生产者插入元素后才继续执行取元素!!

接下来通过"循环队列"来实现一个阻塞队列

3.阻塞队列实现

实现阻塞队列是要实现一个普通队列然后加上"阻塞功能"

这里我们使用循环队列实现阻塞队列,下面是循环队列的三种状态

我们先写一个普通的循环队列的入队和出队操作

class MyBlockingQueue{
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    //入队列
    public void put(int value){
        if(size == items.length){
            //队列满了.不能插入
            return;
        }
        items[tail] = value;
        tail++;
        //针对tail的处理
        //1)这个写法非常常见
        //tail = (tail+1)%items.length;
        //2)可读性好并且比求余的代码效率高
        if(tail >= items.length){
            tail = 0;
        }
        //插入成功
        size++;

    }
    //出队列
    public Integer take(){
        if(size == 0){
            //队列为空,不能出队
            return null;
        }
        int result = items[head];
        head++;
        if(head >= items.length){
            head = 0;
        }
        size--;
        return result;
    }
}

我们在队列中插入几个元素并取出

 public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);
        int result = 0;
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
        result = queue.take();
        System.out.println(result);
    }

现在我们在普通队列的基础上加上阻塞功能

阻塞功能意味着该队列是要在多线程环境下使用的

多线程环境下要保证线程安全,需要给方法加锁
使用wait()notify()方法添加阻塞功能
当队列为空时和队列满时都需要阻塞
sleep()方法是指定休眠的时间后唤醒,但是我们不能确定指定的时间是多少,需要看程序运行情况

修改后:

public void put(int value){
        synchronized (this){
            if(size == items.length){
                //队列满了.不能插入
                //return;
                //阻塞
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            items[tail] = value;
            tail++;
            //针对tail的处理
            //1)这个写法非常常见
            //tail = (tail+1)%items.length;
            //2)可读性好并且比求余的代码效率高
            if(tail >= items.length){
                tail = 0;
            }
            //插入成功
            size++;
            //唤醒队列为空处的wait()
            this.notify();
        }

    }
    //出队列
    public Integer take(){
        int result = 0;
        synchronized (this){
            if(size == 0){
                //队列为空,不能出队
                //return null;
                //阻塞
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            result = items[head];
            head++;
            if(head >= items.length){
                head = 0;
            }
            size--;
            //
            //唤醒队列满的wait()
            this.notify();
        }
        return result;
    }

上述代码还有个问题

如果notifyAll()了,这里的wait()一定会被唤醒!但是该线程还没有抢占到锁,当锁被这个线程抢占到时,队列的状态可能会是满的,因此我们最好用while循环,然后继续判断队列状态

这样是比较稳妥的方法

我们使用MyBlockingQueue再写一个生产者消费者模型,看是否能达到效果

MyBlockingQueue queue = new MyBlockingQueue();
        Thread customer = new Thread(()->{
            while(true){
                int result = queue.take();
                System.out.println("消费: "+result);
            }
        });
        customer.start();
        Thread producer = new Thread(()->{
            int count = 0;
            while(true){
                System.out.println("生产者: "+count);
                queue.put(count);
                count++;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();

也是成对出现的,只有生产者向队中插入了,消费者才能获取

我们调整代码,让消费者速度降低

再来看结果

生产者生产的数据将循环队列充满后开始阻塞,消费者线程休眠结束后开始获取数据,获取一个后,阻塞队列便出现一个空位置,唤醒put的wait()后,继续生产数据,然后又阻塞等待队列不满.....

至此就用循环队列实现了阻塞队列

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
《计算机操作系统》课程设计 题 目: 生产者---消费者问题 专 业: 软件工程 年 级: 2010级 小组成员: A B 指导教师: 时 间: 地 点: 2012年 5 月 摘要 生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区 的线程——即所谓的"生产者"和"消费者"——在实际运行时会发生的问题。生产者的主要作 用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区 消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也 不会在缓冲区中空时消耗数据。 生产者消费者模是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消 费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不 用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队 列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。 目录 1. 概述 4 2. 课程设计任务及要求 4 2.1 设计任务 4 2.2 设计要求 4 2.3 分工日程表 4 3. 算法及数据结构 4 3.1算法的总体思想 4 3.2 生产者模块 4 3.3 消费者模块 6 4. 程序设计与实现 7 4.1 程序流程图 7 4.2 程序代码 9 4.3 实验结果 14 5. 结论 16 6. 收获、体会和建议 16 6.1收获 16 7. 参考文献 17 1. 概述 本课题设计是完成了"操作系统原理"课程进行的一次全面的综合训练,通过这次课程 设计,充分检验学生对课程的掌握程度和熟练情况,让学生更好的掌握操作系统的原理 及其实现方法,加深对课程的基础理论和算法的理解,加强学生的动手能力。 2. 课程设计任务及要求 2.1 设计任务 通过研究Linux 的进程机制和信号量实现生产者消费者问题的并发控制. 说明:有界缓冲区内设有20个存储单元,放入/取出的数据项设定为1- 20这20个整型数。 2.2 设计要求 (1)每个生产者和消费者对有界缓冲区进行操作后,实时显示有界缓冲区的全部内容 、当前指针位置和生产者/消费者的标识符。 (2)生产者和消费者各有两个以上。 (3)多个生产者或多个消费者之间须有共享对缓冲区进行操作的函数代码。 提示:(1) 有界缓冲区可用数组实现。 2.3 分工日程表 " "周三下午 "周四上午 "周四下午 "周五上午 "周五下午 " "A "分析题目 "讨论,分工"编写代码 "测试系统 "编写文档 " "B "分析题目 "讨论,分工"编写代码 "添加备注 "完善系统 " 3. 算法及数据结构 3.1算法的总体思想 在同一个进程地址空间内执行的两个线程。 生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。 消费者线程从缓冲区中获得物品,然后释放缓冲区。 当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者 线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费 者线程将被阻塞,直到新的物品被生产出来。 3.2 生产者模块 3.2.1 功能 在同一个进程地址空间内执行的两个线程。生产者线程生产物品,然后将物品放 置在一个空缓冲区中供消费者线程消费。当生产者线程生产物品时,如果没有空缓冲 区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。 3.2.2 数据结构 producer_semaphore//生产者的资源信号量(初始值为缓冲区的大小) Buffer[pn] //有界缓冲区 Pn ///缓冲区目标位置 MAX_BUFFER//缓冲区上限 buffer_mutex//互斥信号量 Wait()//等待操作,用于申请资源 Signal()//信号操作,用于释放资源 Sleep()//挂起 3.2.3 算法 "void *producer_thread(void *tid){ " "pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL); " "/* 设置状态,PTHREAD_CANCEL_ENABLE是正常处理cancel信号*/ " "while(1){ " "sem_wait(&producer_semaphore); /*等待,需要生存*/ " "srand((int)time(NULL)*(int)tid); " "sleep(rand()%2+1); /*一个或两个需要生产*/ " "while((produce_pointer+1)%20==consume_pointer); /*指
### 回答1: 假设我们有一个多线程应用程序,每个线程都需要使用相同的 API token 来访问某个服务。我们可以使用线程安全的方来共享这个 token。 一种方法是将 token 存储在一个全局变量中,并使用锁来保证每个线程访问该变量时的原子性。例如,我们可以使用 Python 中的 threading 库: ```python import threading TOKEN = "my_api_token" TOKEN_LOCK = threading.Lock() def use_token(): with TOKEN_LOCK: # 使用 TOKEN 访问 API pass # 创建多个线程并启动它们 threads = [] for i in range(10): thread = threading.Thread(target=use_token) thread.start() threads.append(thread) # 等待所有线程完成 for thread in threads: thread.join() ``` 在上面的示例中,我们将 token 存储在全局变量 TOKEN 中,并使用 threading.Lock() 创建一个锁对象 TOKEN_LOCK。然后在每个线程中,我们使用 with 语句来获取锁对象,这样可以确保每个线程在访问 TOKEN 变量时是安全的。 当一个线程获取到锁对象时,其他线程将被阻塞,直到该线程释放锁对象。这样可以确保每个线程都能够安全地访问 TOKEN 变量,而不会与其他线程产生冲突。 需要注意的是,使用全局变量和锁来共享 token 只是其中的一种解决方案。我们还可以考虑使用线程本地存储(Thread-Local Storage)或者单例模等其他的技术来实现共享 token 的方。具体的实现方会根据具体的应用场景和编程语言而有所不同。 ### 回答2: 多线程公用token案例中,多个线程需要同时访问一个共享的token资源。在这种情况下,需要确保多个线程能够正确地并发访问和更新token,且不会出现数据竞争和错误的结果。 首先,可以创建一个全局的token变量,作为公共资源。在访问和更新token时,需要采取适当的同步机制来保证线程安全。可以使用互斥锁或信号量来实现线程间的互斥操作。在每个线程中访问或更新token之前,需要先获取锁或信号量,操作完成后再释放锁或信号量。 其次,需要考虑在多线程环境下对token的使用和更新策略。可以使用条件变量或线程信号量来控制线程的执行顺序和条件等待。例如,可以设定一个条件,当token的值满足一定条件时,线程可以执行相应的操作,否则需要等待条件满足。这样可以确保线程在合适的时机执行,避免了不必要的等待和资源浪费。 另外,还可以利用队列或缓冲区来处理并发请求。当多个线程同时请求token时,可以将请求按照先后顺序加入到队列或缓冲区中,并由一个线程作为调度者负责分发token。这样可以避免竞争和冲突,并确保所有线程都能够公平地使用token。 综上所述,多线程公用token案例需要通过合适的同步机制、条件等待和任务分发等策略来保证线程安全和公平性。通过合理的设计和实现,可以确保多线程能够正确地并发使用共享的token资源,提高系统的性能和可靠性。 ### 回答3: 多线程公用token案例是指多个线程同时操作一个共享的token对象。在这种情况下,线程之间会竞争和共享token的资源,可能会产生冲突和并发安全问题。 为了解决多线程公用token的问题,可以采取以下方法: 1. 锁机制:可以使用互斥锁或信号量等机制来保证多个线程对token对象的访问互斥进行,每次只有一个线程能够获得token的使用权,其他线程需要等待。这样能够避免并发冲突问题。 2. 线程安全的数据结构:可以使用线程安全的数据结构来管理token,例如使用ConcurrentHashMap或ConcurrentLinkedQueue等,并采用原子操作来实现对token的安全访问。 3. 同步方法或关键字:可以使用synchronized关键字修饰对token的操作方法,使得每个线程在执行该方法时,都会获得token的独占访问权,其他线程需要等待。 4. 使用线程池:可以使用线程池来管理多线程的执行,通过控制线程池中线程的数量和调度机制,来控制并发的访问token对象,从而减少冲突。 5. 分配独立的token对象:可以为每个线程分配独立的token对象,让每个线程操作自己的token对象,避免多个线程之间的竞争和冲突。 总之,多线程公用token的案例需要注意并发安全问题,并采取相应的安全措施,以保证线程之间对token的访问安全和正确。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoLo♪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值