阻塞队列中的线程协作(阻塞、唤醒、锁)

自己写一个阻塞队列

阻塞队列,主要操作有两个,一个是put放入元素,另一个是take取出元素。所谓的阻塞就是当多个线程同时存取数据时,如果遇到队列为空或者队列为满时,会发生阻塞。并且多个线程同时执行take或者put操作时,某一时刻只有一个线程获得执行权利,也就是执行任何一个操作之前需要获得锁,没有获得锁的线程发生阻塞。

put: 向队列中存入一个元素,如果已满,则阻塞当前线程,等待唤醒。如果正常存入了元素,那么唤醒其他阻塞的线程(有些执行take操作的线程因为队列为空而阻塞)

take: 从队列中取一个元素,如果队列为空,则阻塞当前线程,等待唤醒。如果正常取出了元素,那么唤醒其他阻塞的线程(有些执行put操作的线程因为队列满而阻塞)

Object类提供了几个操作来进行当前线程的唤醒和阻塞
wait: 阻塞当前线程,其实就是将当前线程放入当前对象的等待集中,释放锁(如果持有锁的话),暂停当前线程。
notify: 唤醒当前对象等待集上的一个线程。
notifyAll: 唤醒当前对象等待集上的所有线程。

基于以上,我们实现一个自己的阻塞队列:

public class MyBlockingQueue1<T> implements MyBlockingQueue<T>{
    private Object[] array;
    private int count=0;
    private int getIndex=0;
    private int putIndex=0;
    public MyBlockingQueue1(int cap){
        array = new Object[cap];
    }
    public synchronized void put(T ele) throws InterruptedException {
        while (isFull()){
            wait();
        }
        array[putIndex++]=ele;
        if (putIndex>=array.length){
            putIndex=0;
        }
        count++;
        notifyAll();
    }

    public synchronized T take() throws InterruptedException {
        while (isEmpty()){
            wait();
        }
        Object element = array[getIndex++];
        if (getIndex>=array.length){
            getIndex=0;
        }
        count--;
        notifyAll();
        @SuppressWarnings("unchecked")
        T t = (T)element;
        return t;
    }
    private boolean isEmpty(){
        return count==0;
    }
    private boolean isFull(){
        return  count>=array.length;
    }
}

put和take方法都加了synchronized,也就是说这两个方法执行之前都需要先取得同一个对象锁,从而,这两个方法就不可以并行执行。于是我们可以稍微优化一下,比如put和take使用两个不同的锁,这两个操作就不会互相影响了。但也会因此使得count成为了临界资源,count++会发生竞态,我们可以考虑使用一个原子变量类来替代int类型。而且上面介绍提到的唤醒部分,每当成功put或者成功take,我们都唤醒所有线程,其实put操作成功时,我们只想唤醒那些因为队列为空而阻塞的线程,take操作成功时,我们只想唤醒那些因为队列已满而阻塞的线程,而且唤醒一个就够了。于是我们可以使用Condition来使得线程在两个不同的等待队列上进行等待,每次都唤醒特定队列上的一个线程。于是0.2版代码如下:

public class MyBlockingQueue2<T> implements MyBlockingQueue<T>{

    private Object[] array;
    private AtomicInteger count=new AtomicInteger(0);//临界资源,使用原子变量类
    private int getIndex=0;
    private int putIndex=0;

    private ReentrantLock putLock = new ReentrantLock();
    private final Condition notEmpty = putLock.newCondition();//防止过早唤醒
    private ReentrantLock takeLock = new ReentrantLock();
    private final Condition notFull = takeLock.newCondition();//防止过早唤醒

    public MyBlockingQueue2(int cap){
        array = new Object[cap];
    }
    public void put(T ele) throws InterruptedException {
        try {
            putLock.lock();
            while (isFull()){
                notFull.await();
            }
            array[putIndex++]=ele;
            if (putIndex>=array.length){
                putIndex=0;
            }
            int c = count.getAndIncrement();
            if (c==0){
                notEmpty.signal();
            }
        }finally {
            putLock.unlock();
        }

    }

    public  T take() throws InterruptedException {
        try {
            takeLock.lock();
            while (isEmpty()){
                notEmpty.wait();
            }
            Object element = array[getIndex++];
            if (getIndex>=array.length){
                getIndex=0;
            }
            int c = count.getAndDecrement();
            if (c==array.length){
                notFull.signal();
            }
            @SuppressWarnings("unchecked")
            T t = (T)element;
            return t;
        }finally {
            takeLock.unlock();
        }

    }
    private boolean isEmpty(){
        return count.get()==0;
    }
    private boolean isFull(){
        return  count.get()>=array.length;
    }
}

JDK中的阻塞队列实现

我们自己写的这个阻塞队列只是实现了最基本的put和take两个操作,而jdk中的阻塞队列提供的功能更加全面一些。首先,提供了put和take对应的非阻塞方法offer和poll,这两个方法,即使遇到队列为满或为空的情况,也不会阻塞当前线程,而是直接返回false或null。并且还提供了阻塞时间选项,比如,poll时,如果队列为空,可以选择阻塞x秒,如果x秒内还是没能拿到元素,则返回null。其次还提供了比如drainTo、contains、remove等方法来完成一次性取出所有元素,判断元素存在与否,移除一个元素等操作,作为阻塞队列的接口BlockingQueue主要有四个实现类:
ArrayBlockingQueue:这个是用数组实现的一个阻塞队列,put和take使用了同一个锁,线程等待队列使用了Condition。
LinkedBlockingQueue:这个是用链表实现的一个阻塞队列,put和take使用了 两个锁,理论上支持更大的并发量。
还有就是PriorityBlockingQueue和SynchronousQueue,一个是优先级阻塞队列,每次都按照优先级来存取元素,另一个是同步队列,其实它内部没有维护队列,而是存入一个元素之后,必须有其他线程将他取走,不然再想put的线程就会被阻塞。这两个队列内部实现跟前两个有所不同,看起来要更复杂一点,比如PriorityBlockingQueue内部是通过堆来维护优先级的,优先级比对我我们可以存入自己的比较器,而SynchronousQueue内部通过Transferer分装了一些操作,这两个队列待独立一篇细说。

完整代码请访问小火箭

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java阻塞队列(Blocking Queue)是一个多线程并发编程常用的数据结构,它可以在某些条件下挂起线程,并在条件满足时自动唤醒阻塞队列提供了一种线程安全的方式来实现生产者-消费者模型。 阻塞队列位于java.util.concurrent包,简称JUC。它提供了许多与多线程并发相关的组件操作。阻塞队列可以通过以下方式进行初始化: ``` BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(); //基于链表来实现,可以指定阻塞队列的大小 ``` 阻塞队列的核心方法有以下几种类型: 1. 插入方法:add(e)、offer(e)、put(e)、offer(e,time,unit) - 如果队列已满,再往队列插入元素会抛出异常或返回特殊值。 2. 移除方法:remove()、poll()、take()、poll(time,unit) - 如果队列为空,从队列移除元素会抛出异常或返回特殊值。 3. 检查方法:element()、peek() - 检查队列的头部元素,如果队列为空,element()方法会抛出异常,peek()方法会返回null。 阻塞队列的特性包括: - 抛出异常:当阻塞队列满时,再往队列添加元素会抛出异常IllegalStateException,当阻塞队列空时,移除队列的元素会抛出异常NoSuchElementException。 - 特殊值:插入方法成功时返回true,失败时返回false;移除方法成功时返回队列的元素,队列为空时返回null。 - 一直阻塞:当阻塞队列满时,生产者线程继续往队列放入元素,队列会一直阻塞生产者线程,直到生产者线程put数据或响应断退出;当阻塞队列空时,消费者线程试图从队列取出元素,队列会一直阻塞消费者线程,直到队列可用。 - 超时退出:当阻塞队列满时,队列阻塞线程一定时间,超过时间后生产者线程会退出。 可以通过创建多个线程作为生产者或消费者来使用阻塞队列。以下是一个示例: ```java public static void main(String[] args) { BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); //作为交易场所 Thread t1 = new Thread() { //作为生产者 public void run() { for (int i = 0; i < 1000; i++) { try { queue.put(i); System.out.println("生产元素生产了" + i + "个"); sleep(1000); //每秒钟生产一个元素 } catch (InterruptedException e) { e.printStackTrace(); } } } }; t1.start(); Thread t2 = new Thread() { //作为消费者 public void run() { while (true) { //频繁取队首元素 int num = queue.take(); System.out.println("消费元素为" + num); } } }; t2.start(); } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值