一、引入
我们刚刚已经学习完了 等待唤醒机制
最基本的写法,其实它还有第二种方式:利用阻塞队列实现。
那什么是阻塞队列呢?其实就好比是连接生产者和消费者之间的管道。
以刚刚的故事为例,厨师做好面条后,就可以将面条放在管道中,而且左边的消费者就可以从管道中获取面条去吃。
而且更重要的是,我们去规定管道中最多能放多少碗面条。
![g6r6t-ftk7d](https://img-blog.csdnimg.cn/img_convert/0150a5823efc56ba6c425ace78602c27.gif)
如果我们规定管道中只能放一碗面条,那么运行结果就是和我们上面写的代码是一样的,做一碗吃一碗,做一碗吃一碗。
中间的这个管道其实就是阻塞队列。
阻塞队列可以分成两个单词去理解:
-
队列:数据在管道中就好比是排队一样,先放进去的这碗面,是最先被拿出来的,所以就叫做队列。
-
阻塞:当厨师put数据时,如果中间的管道已经放满了,此时厨师就会等着,因此将等着,什么也干不了的这个动作叫做
阻塞
吃货其实也会阻塞的,当吃货从管道中获取不到数据了,此时它也只能等着,什么也干不了,这个动作也叫作阻塞。
知道了什么是阻塞队列,接下来看看阻塞队列的体系结构。
二、阻塞队列的继承结构
阻塞队列一共实现了四个接口
-
最顶层的接口是
Iterable
,也就表示,阻塞队列是可以利用迭代器/增强for进行遍历的。 -
阻塞队列本身还实现了
Collection接口
,由此可见,阻塞队列其实就是一个单列集合。 -
Queue
:它表示队列 -
BlockingQueue
:表示是阻塞队列
上面四个都是接口,不能直接创建它们的对象,我们要创建的是两个实现类的对象。
ArrayBlockingQueue
、LinkedBlockingQueue
,它们有什么区别呢?
ArrayBlockingQueue
:底层是数组实现的,有界(即有长度的界限)。我们在创建 ArrayBlockingQueue
对象的时候,必须要去指定队列的长度。
LinkedBlockingQueue
:底层是链表实现的,无界(指没有长度的界限),创建它的对象的时候我们不需要去指定队列的长度。但是它又并不是真正的无界,它其实也是有最大值的,只不过这个最大值非常的大,是int的最大值,有21个亿那么多。
三、用阻塞队列完成 唤醒机制
代码实现
用阻塞队列完成 唤醒机制
,代码非常的简单。
只不过在这里我们有个小细节一定要注意:生产者和消费者必须使用同一个阻塞队列才行。
因此阻塞队列的对象
的代码最好写在测试类中,有了这个对象后,我们再通过创建对象的方式将队列传递给 Cook
、foodie
,这样就可以实现两者用的是同一个阻塞队列。
public class ThreadDemo {
public static void main(String[] args) {创建阻塞队列的对象,泛型表示的是队列中数据的类型
//通过刚刚的学习我们知道,ArrayBlockingQueue是一个有界的阻塞队列,因此在创建它对象的时候必须指定上线
//这里假设它最多只能放1个
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
}
}
细节:这里是不需要写锁的,看一下源码就知道了,put()
底层已经有锁了。
选择 ArrayBlockingQueue
的 put()
跟进,可以发现在方法中,它首先会用 lock锁
的方式将代码锁起来。
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
//获取到锁对象
final ReentrantLock lock = this.lock;
//然后用锁对象去调用一个方法,这个方法就可以来获取锁。
lock.lockInterruptibly();
try {
//然后进行循环判断,数据的个数 跟 队列的长度是否相等。
while (count == items.length)
//如果相等,说明队列装满了,等待
notFull.await();
//没有装满就往队列中放数据就行了
enqueue(e);
} finally {
//当这些逻辑执行完毕后,再调用unlock()释放锁
lock.unlock();
}
}
并且take()的底层也是有锁的,我们在写代码的时候也不需要去加锁了,否则就会有锁的嵌套,锁的嵌套容易导致死锁。
public E take() throws InterruptedException {
//先获取到锁对象
final ReentrantLock lock = this.lock;
//然后再去调用lock方法获取到锁
lock.lockInterruptibly();
try {
//在下面它又写了一个循环,来判断队列中数据是不是0
while (count == 0)
//如果是0表示没数据,拿不出来只能等着
notEmpty.await();
//如果有数据就获取
return dequeue();
} finally {
//当上面所有代码执行完毕后,再调用unlock()释放锁
lock.unlock();
}
}
代码示例
public class Cook extends Thread{
//定义一个成员变量,表示阻塞队列,只不过此时只是只定义不给值。创建对象的时候才赋值。
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断的把面条放到阻塞队列当中
try {
//异常直接try即可
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
----------------------------------
public class Foodie extends Thread{
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while(true){
//不断从阻塞队列中获取面条
try {
//获取面条,获取的方法叫take()
//并且take()的底层也是有锁的,我们在写代码的时候也不需要去加锁了
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
-----------------------------------------------
public class ThreadDemo {
public static void main(String[] args) {创建阻塞队列的对象,泛型表示的是队列中数据的类型
//通过刚刚的学习我们知道,ArrayBlockingQueue是一个有界的阻塞队列,因此在创建它对象的时候必须指定上线
//这里假设它最多只能放1个
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//2.创建线程的对象,并把阻塞队列传递过去
Cook c = new Cook(queue);
Foodie f = new Foodie(queue);
//3.开启线程
c.start();
f.start();
}
}
程序运行完毕,发现结果怎么跟想象的不太一样,这里怎么会有连续的?
![image-20240507072826227](https://img-blog.csdnimg.cn/img_convert/7603e057f42b042b7a67828bdadb7245.png)
我们刚刚控制台看到的运行结果是这两条打印语句导致的。
但打印语句其实是定义在锁的外面,定义在锁的外面其实就是会导致这个现象。
当我们将之前写的等待唤醒机制的代码进行改写,将打印语句放到锁的外面,发现也会这样的情况
![image-20240507073408049](https://img-blog.csdnimg.cn/img_convert/c3c2acbaed1b5be07a9e3c76017a3d26.png)
因此连续的原因就是因为我们将打印语句写在了锁的外面,导致看上去数据错乱的情况。
但是写在锁的外面并不会对数据的安全造成影响,它没有对共享数据造成任何的修改/改编,它只是对我们看运行的结果造成了一点点不方便而已。