9.2 多线程基础之阻塞队列

本文介绍了阻塞队列和生产者消费者模型。生产者消费者模型可实现解耦合和削峰填谷,提高系统抗风险能力。还提及标准库中的阻塞队列,以及阻塞队列的实现,包括线程安全(加锁并考虑内存可见性)和阻塞功能的实现,同时给出优化建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 阻塞队列是什么

阻塞队列是一种特殊的队列 . 也遵守 " 先进先出 " 的原则 .
阻塞队列能是一种线程安全的数据结构 , 并且具有以下特性 :
当队列 的时候 , 继续 队列就会阻塞 , 直到有其他线程从队列中取走元素 .
当队列 的时候 , 继续 队列也会阻塞 , 直到有其他线程往队列中插入元素 .
阻塞队列的一个典型应用场景就是 " 生产者消费者模型 ". 这是一种非常典型的开发模型 .

2 生产者消费者模型 

image.png

使用生产者消费者模型 , 在工作中是非常频繁的

生产者-消费者模型的优点有很多 , 其中最明显的优点有两条 :

2.1 可以做到更好的 “解耦合”

image.png

2.2  “削峰填谷” , 提高整个系统的抗风险能力

image.png

3. 标准库中的阻塞队列 

 正因为生产者消费者模型很重要, 虽然阻塞队列只是一个数据结构, 但还会把这个数据结构单独是现成一个服务器程序并且使用单独的主机/ 主机集群来部署

Java 标准库中内置了阻塞队列 . 如果我们需要在一些程序中使用阻塞队列 , 直接使用标准库中的即可 .
1. BlockingQueue 是一个接口 . 真正实现的类是 LinkedBlockingQueue.
2. put 方法用于阻塞式的入队列 , take 用于阻塞式的出队列 .
3. BlockingQueue 也有 offer, poll, peek 等方法 , 但是这些方法不带有阻塞特性 .
package thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

// 生产者消费者模型
public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        // 搞一个阻塞队列, 作为交易场所
        // Java 标准库中的阻塞队列
        // BlockingQueue 是父类,我们要实例化他的接口
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        // 负责生产元素
        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    // BlockingQueue 虽然也支持 offer、poll 等普通队列的方法
                    // 但是仍然推荐大家使用 put 来入队列,使用 take 来出队列(可以做到阻塞)
                    queue.put(count);
                    System.out.println("生产元素" + count);
                    count++;

                    Thread.sleep(1000); // 方便观察
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 负责消费元素
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    Integer n = queue.take();
                    System.out.println("消费元素" + n);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t2.start();
    }
}

4. 阻塞队列实现

1. 通过 " 循环队列 " 的方式来实现 .
2. 使用 synchronized 进行加锁控制 .
3. put 插入元素的时候 , 判定如果队列满了 , 就进行 wait. ( 注意 , 要在循环中进行 wait. 被唤醒      时不一定队列就不满了, 因为同时可能是唤醒了多个线程 ).
4. take 取出元素的时候 , 判定如果队列为空 , 就进行 wait. ( 也是循环 wait)

第一步: 

class MyBlockingQueue {
    //使用一个string 类型的数组来保存元素, 假设这里只存String
    private String[] items = new String[1000];
    // 指向队列的头部
    private  int head = 0;
    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    // 当head 和 tail 相等(重合), 相当于空的队列
    private int tail = 0;
    // 使用size来表示元素个数
    private int size = 0;

    // 入队列
    public void put(String elem) {
        if(size >= items.length) {
            //队列满了
            return;
        }
        items[tail] = elem;
        tail++;
        if(tail >= items.length) {
            tail = 0;
        }
        size++;
    }
    // 出队列
    public String take() {
        if(size == 0) {
            // 队列为空, 暂时不能出队列
            return null;
        }
        String ret = items[head];
        head++;
        if(head >= items.length) {
            head = 0;
        }
        size--;
        return ret;
    }
}
public class Demo19 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put("aaa");
        queue.put("bbb");
        queue.put("ccc");

        String elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
    }
}

目前已经完成了最基本的队列的实现
接下来 , 我们就实现阻塞队列
阻塞队列需要满足两点 :

1. 线程安全 : 通过加锁实现

 写操作涉及修改, 必须加锁. 并且既然需要修改的地方这么多, 就不容直接把代码全部加锁image.png

class MyBlockingQueue {
    //使用一个string 类型的数组来保存元素, 假设这里只存String
    private String[] items = new String[1000];
    // 指向队列的头部
    private  int head = 0;
    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    // 当head 和 tail 相等(重合), 相当于空的队列
    private int tail = 0;
    // 使用size来表示元素个数
    private int size = 0;

    // 入队列
    public void put(String elem) {
        // 此处的写法就相当于直接把synchronized 加到方法上了
        synchronized (this) {
            if (size >= items.length) {
                //队列满了
                return;
            }
            items[tail] = elem;
            tail++;
            if (tail >= items.length) {
                tail = 0;
            }
            size++;
        }
    }
    // 出队列
    public String take() {
        synchronized (this) {
            if (size == 0) {
                // 队列为空, 暂时不能出队列
                return null;
            }
            String elem = items[head];
            head++;
            if (head >= items.length) {
                head = 0;
            }
            size--;
            return elem;
        }
    }
}

考虑内存可见性问题

我们在每个变量前面加上 volatile 关键字 , 因为这段代码既有读 , 又有写 , 为了防止内存可见性 , 所以加上 volatile关键字

//使用一个string 类型的数组来保存元素, 假设这里只存String
    volatile private String[] items = new String[1000];
    // 指向队列的头部
    volatile private  int head = 0;
    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    // 当head 和 tail 相等(重合), 相当于空的队列
    volatile private int tail = 0;
    // 使用size来表示元素个数
    volatile private int size = 0;

 2. 实现阻塞: 

a) 当队列满的时候, 在进行put 就会产生阻塞

b) 当队列空的时候, 再进行take 也会产生阻塞

class MyBlockingQueue {
    //使用一个string 类型的数组来保存元素, 假设这里只存String
    volatile private String[] items = new String[1000];
    // 指向队列的头部
    volatile private  int head = 0;
    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    // 当head 和 tail 相等(重合), 相当于空的队列
    volatile private int tail = 0;
    // 使用size来表示元素个数
    volatile private int size = 0;

    // 入队列
    public void put(String elem) throws InterruptedException {
        // 此处的写法就相当于直接把synchronized 加到方法上了
        synchronized (this) {
            if (size >= items.length) {
                //队列满了
//                return;
                this.wait();
            }
            items[tail] = elem;
            tail++;
            if (tail >= items.length) {
                tail = 0;
            }
            size++;
            this.notify();
        }
    }
    // 出队列
    public String take() throws InterruptedException {
        synchronized (this) {
            if (size == 0) {
                // 队列为空, 暂时不能出队列
//                return null;
                this.wait();
            }
            String elem = items[head];
            head++;
            if (head >= items.length) {
                head = 0;
            }
            size--;
            //使用这个notify 唤醒队列满的阻塞情况
            this.notify();
            return elem;
        }
    }
}

 

image.png 优化

上述代码中, 满足条件就进行wait;   当wait被唤醒之后, 条件就一定被打破了吗?

例如, 当前put操作因为队列满, 转为wait阻塞.  过了一阵, wait被唤醒了, 唤醒的时候, 此时的队列一定就不满了嘛?  是否可能队列还是满着呢? 万一出现了, 唤醒之后,队列还是满着的, 此时意味着接下来的代码操作就有可能把之前存入的元素给覆盖了. 

while (size >= items.length) {
    // 队列满了
    // return;
    this.wait();
}
while (size == 0) {
    // 队列为空, 暂时不能出队列
    // return null;
    this.wait();
}

 此处while的目的不是为了循环, 而是借助循环的方式,  巧妙的实现wait醒了之后再次确认一下是否满足条件, 避免数据覆盖等问题

使用wait时候, 建议搭配while进行条件判定

 完整代码

class MyBlockingQueue {
    //使用一个string 类型的数组来保存元素, 假设这里只存String
    volatile private String[] items = new String[1000];
    // 指向队列的头部
    volatile private  int head = 0;
    // 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
    // 当head 和 tail 相等(重合), 相当于空的队列
    volatile private int tail = 0;
    // 使用size来表示元素个数
    volatile private int size = 0;

    // 入队列
    public void put(String elem) throws InterruptedException {
        // 此处的写法就相当于直接把synchronized 加到方法上了
        synchronized (this) {
            while (size >= items.length) {
                //队列满了
//                return;
                this.wait();
            }
            items[tail] = elem;
            tail++;
            if (tail >= items.length) {
                tail = 0;
            }
            size++;
            this.notify();
        }
    }
    // 出队列
    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                // 队列为空, 暂时不能出队列
//                return null;
                this.wait();
            }
            String elem = items[head];
            head++;
            if (head >= items.length) {
                head = 0;
            }
            size--;
            //使用这个notify 唤醒队列满的阻塞情况
            this.notify();
            return elem;
        }
    }
}
public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();

        Thread t1 = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    queue.put(count + "");
                    System.out.println("生产元素:" + count);
                    count ++;
                    // 生产速度较慢
                    Thread.sleep(1000);
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(() -> {
           while (true) {
               try {
                    String count = queue.take();
                    System.out.println("消费元素:" + count);
               }catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
        });

        t1.start();
        t2.start(); 
    }
}

参考: Java Web 实战 08 - 多线程基础之阻塞队列_加勒比海涛的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值