浅谈阻塞队列的原理

本文详细解析了阻塞队列在生产者消费者模型中的应用,介绍了BlockingQueue的四种行为模式,以及ArrayBlockingQueue、DelayQueue等实现类的工作原理。重点讲解了如何利用Lock和Condition接口创建自定义的阻塞队列,以实现线程间的精细通信。

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

阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmhzIyRO-1606749754423)(C:\Users\DELL\AppData\Roaming\Typora\typora-user-images\image-20201130162019105.png)]

四个 API

BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:

在这里插入图片描述

  • 抛异常:如果试图的操作无法立即执行,抛一个异常。
  • 特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
  • 阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
  • 超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是true / false)。

无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个 NullPointerException。


BlockingQueue 的实现类

BlockingQueue 是个接口,你需要使用它的实现之一来使用BlockingQueue,java.util.concurrent包下具有以下 BlockingQueue 接口的实现类:

在这里插入图片描述

  • ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
  • DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
  • LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
  • SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

阻塞队列实现原理

首先要先了解一下 Condition 接口
lock中使用多条件condition

Java 并发包下的提供Lock,Lock相对于Synchronized可以更好的解决线程同步问题,更加的灵活和高效,并且ReadWriteLock锁还能实现读、写的分离。但线程间仅仅互斥是不够的,还需要通信,本篇的内容是基于上篇之上,使用Lock如何处理线程通信。阻塞队列(BlockingQueue)就是使用condition的和lock实现的。

在这里插入图片描述

  • Condition 将 Object的通信方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 通信方法的使用。
  • 在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
  • Condition的强大之处在于它可以为多个线程间建立不同的Condition, 使用synchronized/wait()只有一个阻塞队列,notifyAll会唤起所有阻塞队列下的线程,而使用lock/condition,可以实现多个阻塞队列,signalAll只会唤起某个阻塞队列下的阻塞线程。
  • Condition 是由 Lock维护的,在AQS中有他的对象,它通过 Node 维护一个队列,里面存放的是线程。

我们看一下他们两者的区别,传统的生产者消费者代码:

//模拟生产和消费的对象
class Buffer {
    private int	maxSize;
    private List<Date> storage;
    Buffer(int size){
        maxSize=size;
        storage=new LinkedList<>();
    }
    //生产方法
    public synchronized void put()  {
        try {
            while (storage.size() ==maxSize ){//如果队列满了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                wait();//阻塞线程
            }
            storage.add(new Date());
            System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
            Thread.sleep(1000);
            notifyAll();//唤起线程
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }		
    }
    //消费方法
    public synchronized void take() {
        try { 
            while (storage.size() ==0 ){//如果队列满了
                System.out.print(Thread.currentThread().getName()+": wait \n");;
                wait();//阻塞线程
            }
            Date d=((LinkedList<Date>)storage).poll();
            System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
            Thread.sleep(1000);
            notifyAll();//唤起线程
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }		
    } 
}
//生产者
class Producer implements Runnable{
	private Buffer buffer;
	Producer(Buffer b){
		buffer=b;
	}
	@Override
	public void run() {
		while(true){
			buffer.put();
		}
	}	
}
//消费者
class Consumer implements Runnable{
	private Buffer buffer;
	Consumer(Buffer b){
		buffer=b;
	}
	@Override
	public void run() {
		while(true){
			buffer.take();
		}
	}	
}
//
public class Main{
	public static void main(String[] arg){
		Buffer buffer=new Buffer(10);
		Producer producer=new Producer(buffer);
		Consumer consumer=new Consumer(buffer);
		//创建线程执行生产和消费
		for(int i=0;i<3;i++){
			new Thread(producer,"producer-"+i).start();
		}
		for(int i=0;i<3;i++){
			new Thread(consumer,"consumer-"+i).start();
		}
	}
}

使用lock/condition实现生产者消费者模式如下:

class Buffer {
	private  final Lock lock;
	private  final Condition notFull;
	private  final Condition notEmpty;
	private int	maxSize;
	private List<Date> storage;
	Buffer(int size){
		//使用锁lock,并且创建两个condition,相当于两个阻塞队列
		lock=new ReentrantLock();
		notFull=lock.newCondition();
		notEmpty=lock.newCondition();
		maxSize=size;
		storage=new LinkedList<>();
	}
	public void put()  {
		lock.lock();
		try {   
			while (storage.size() ==maxSize ){//如果队列满了
				System.out.print(Thread.currentThread().getName()+": wait \n");;
				notFull.await();//阻塞生产线程
			}
			storage.add(new Date());
			System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
			Thread.sleep(1000);			
			notEmpty.signalAll();//唤醒消费线程
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{	
			lock.unlock();
		}
	}

	public	void take() {		
		lock.lock();
		try {  
			while (storage.size() ==0 ){//如果队列满了
				System.out.print(Thread.currentThread().getName()+": wait \n");;
				notEmpty.await();//阻塞消费线程
			}
			Date d=((LinkedList<Date>)storage).poll();
			System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
			Thread.sleep(1000);			
			notFull.signalAll();//唤醒生产线程
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}finally{
			lock.unlock();
		}
	} 
}

class Producer implements Runnable{
	private Buffer buffer;
	Producer(Buffer b){
		buffer=b;
	}
	@Override
	public void run() {
		while(true){
			buffer.put();
		}
	}	
}
class Consumer implements Runnable{
	private Buffer buffer;
	Consumer(Buffer b){
		buffer=b;
	}
	@Override
	public void run() {
		while(true){
			buffer.take();
		}
	}	
}
public class Main{
	public static void main(String[] arg){
		Buffer buffer=new Buffer(10);
		Producer producer=new Producer(buffer);
		Consumer consumer=new Consumer(buffer);
		for(int i=0;i<3;i++){
			new Thread(producer,"producer-"+i).start();
		}
		for(int i=0;i<3;i++){
			new Thread(consumer,"consumer-"+i).start();
		}
	}
}

当生产者执行put方法时,调用notEmpty.signalAll()只会唤醒notEmpty.await()下的消费者线程。
当消费者执行塔克方法时,调用notFull.signalAll()只会唤醒notFull.await()下的消费者线程。


我们先看一下使用阻塞队列的做法:

public class BlockingQueueTest {
    //生产者
    public static class Producer implements Runnable{
        private final BlockingQueue<Integer> blockingQueue;
        private volatile boolean flag;
        private Random random;

        public Producer(BlockingQueue<Integer> blockingQueue) {
            this.blockingQueue = blockingQueue;
            flag=false;
            random=new Random();

        }
        public void run() {
            while(!flag){
                int info=random.nextInt(100);
                try {
                    blockingQueue.put(info);
                    System.out.println(Thread.currentThread().getName()+" produce "+info);
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }               
            }
        }
        public void shutDown(){
            flag=true;
        }
    }
    //消费者
    public static class Consumer implements Runnable{
        private final BlockingQueue<Integer> blockingQueue;
        private volatile boolean flag;
        public Consumer(BlockingQueue<Integer> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run() {
            while(!flag){
                int info;
                try {
                    info = blockingQueue.take();
                    System.out.println(Thread.currentThread().getName()+" consumer "+info);
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }               
            }
        }
        public void shutDown(){
            flag=true;
        }
    }
    public static void main(String[] args){
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(10);
        Producer producer=new Producer(blockingQueue);
        Consumer consumer=new Consumer(blockingQueue);
        //创建5个生产者,5个消费者
        for(int i=0;i<10;i++){
            if(i<5){
                new Thread(producer,"producer"+i).start();
            }else{
                new Thread(consumer,"consumer"+(i-5)).start();
            }
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        producer.shutDown();
        consumer.shutDown();

    }
}

阻塞队列的原理:

  • 我们会发现阻塞队列的写法和 Condition 的一些做法有点像,其实我们可以可以从源码看到阻塞队列的底层实现,使用BlockingQueue封装了根据条件阻塞线程的过程,而我们就不用关心繁琐的await/signal操作了。
public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //创建数组    
        this.items = new Object[capacity];
        //创建锁和阻塞条件
        lock = new ReentrantLock(fair);   
        notEmpty = lock.newCondition();//在Lock里面维护的
        notFull =  lock.newCondition();
    }
//添加元素的方法
public void put(E e) throws InterruptedException {
        checkNotNull(e);
    	//获取当前锁
        final ReentrantLock lock = this.lock;
    	//加锁
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            	//如果队列不满就入队
            	enqueue(e);
        } finally {
            lock.unlock();
        }
    }
 //入队的方法
 private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
     	//唤醒消费线程
        notEmpty.signal();
    }
 //移除元素的方法
 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
 //出队的方法
 private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
     	//唤醒生产线程
        notFull.signal();
        return x;
 }

我们看到实现的原理是使用了 Condition 来实现不同线程的唤醒和等待的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值