【Java】阻塞队列实现等待唤醒机制

一、引入

我们刚刚已经学习完了 等待唤醒机制 最基本的写法,其实它还有第二种方式:利用阻塞队列实现。

那什么是阻塞队列呢?其实就好比是连接生产者和消费者之间的管道。

以刚刚的故事为例,厨师做好面条后,就可以将面条放在管道中,而且左边的消费者就可以从管道中获取面条去吃。

而且更重要的是,我们去规定管道中最多能放多少碗面条。

g6r6t-ftk7d

如果我们规定管道中只能放一碗面条,那么运行结果就是和我们上面写的代码是一样的,做一碗吃一碗,做一碗吃一碗。

中间的这个管道其实就是阻塞队列。

阻塞队列可以分成两个单词去理解:

  • 队列:数据在管道中就好比是排队一样,先放进去的这碗面,是最先被拿出来的,所以就叫做队列。

  • 阻塞:当厨师put数据时,如果中间的管道已经放满了,此时厨师就会等着,因此将等着,什么也干不了的这个动作叫做 阻塞

    吃货其实也会阻塞的,当吃货从管道中获取不到数据了,此时它也只能等着,什么也干不了,这个动作也叫作阻塞。

知道了什么是阻塞队列,接下来看看阻塞队列的体系结构。


二、阻塞队列的继承结构

阻塞队列一共实现了四个接口

  • 最顶层的接口是 Iterable,也就表示,阻塞队列是可以利用迭代器/增强for进行遍历的。

  • 阻塞队列本身还实现了 Collection接口,由此可见,阻塞队列其实就是一个单列集合。

  • Queue:它表示队列

  • BlockingQueue:表示是阻塞队列

上面四个都是接口,不能直接创建它们的对象,我们要创建的是两个实现类的对象。

ArrayBlockingQueueLinkedBlockingQueue,它们有什么区别呢?

ArrayBlockingQueue:底层是数组实现的,有界(即有长度的界限)。我们在创建 ArrayBlockingQueue 对象的时候,必须要去指定队列的长度。

LinkedBlockingQueue:底层是链表实现的,无界(指没有长度的界限),创建它的对象的时候我们不需要去指定队列的长度。但是它又并不是真正的无界,它其实也是有最大值的,只不过这个最大值非常的大,是int的最大值,有21个亿那么多。

06_阻塞队列继承结构


三、用阻塞队列完成 唤醒机制 代码实现

用阻塞队列完成 唤醒机制,代码非常的简单。

只不过在这里我们有个小细节一定要注意:生产者和消费者必须使用同一个阻塞队列才行。

因此阻塞队列的对象的代码最好写在测试类中,有了这个对象后,我们再通过创建对象的方式将队列传递给 Cookfoodie,这样就可以实现两者用的是同一个阻塞队列。

public class ThreadDemo {
    public static void main(String[] args) {创建阻塞队列的对象,泛型表示的是队列中数据的类型
        //通过刚刚的学习我们知道,ArrayBlockingQueue是一个有界的阻塞队列,因此在创建它对象的时候必须指定上线
        //这里假设它最多只能放1个
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
    }
}

细节:这里是不需要写锁的,看一下源码就知道了,put() 底层已经有锁了。

选择 ArrayBlockingQueueput() 跟进,可以发现在方法中,它首先会用 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

我们刚刚控制台看到的运行结果是这两条打印语句导致的。

但打印语句其实是定义在锁的外面,定义在锁的外面其实就是会导致这个现象。

image-20240507073000313

当我们将之前写的等待唤醒机制的代码进行改写,将打印语句放到锁的外面,发现也会这样的情况

image-20240507073408049

因此连续的原因就是因为我们将打印语句写在了锁的外面,导致看上去数据错乱的情况。

但是写在锁的外面并不会对数据的安全造成影响,它没有对共享数据造成任何的修改/改编,它只是对我们看运行的结果造成了一点点不方便而已。

  • 35
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java阻塞队列是一种线程安全的数据结构,它提供了同步的功能,用于在多线程环境安全地进行数据交换和通信。其实现原理主要涉及以下几个方面。 首先,阻塞队列实现会使用锁机制确保线程安全。Java可以使用ReentrantLock或synchronized关键字来实现锁,在对队列进行操作时会对其进行加锁,保证同一时刻只有一个线程能够访问队列。 其次,阻塞队列内部会使用条件变量或信号量来实现线程间的协调与通信。当队列为空时,消费者线程需要等待直到队列有数据可取;当队列已满时,生产者线程需要等待直到队列有空位置可放入新数据。通过条件变量或信号量的等待唤醒机制实现了线程间的同步和互斥。 此外,阻塞队列通常还会使用一个循环数组来存储数据。循环数组在插入和删除元素时能够高效地利用已分配的内存空间,避免了频繁的扩容和内存拷贝。同时,循环数组的读写指针可以通过取模运算得到,实现环形循环。 最后,阻塞队列还会根据不同的需求提供不同的阻塞操作。例如,用于插入元素的put()方法在队列已满时会阻塞直到有空位置可用,用于获取元素的take()方法在队列为空时会阻塞直到有数据可取。这些阻塞操作的实现依赖于同步和协调机制,保证了线程安全和数据一致性。 总之,Java阻塞队列通过使用锁、条件变量或信号量、循环数组等机制实现了线程安全和线程间的同步与通信。它是多线程编程常用的工具,能够有效地管理数据的生产和消费,提高多线程程序的可靠性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值