一、阻塞队列的应用场景

阻塞队列可以用来处理生产者--消费者这种模式,

如图所示,有三个生产者线程往阻塞队列里放消息,2个消费者线程从阻塞队列拿消息,消息队列有个容量,

当容量满时要让生产者线程阻塞等待,当队列里没消息时让消费者线程阻塞等待。

阻塞队列用来平衡消费者和生产者

二、自定义简单的阻塞队列

2.1 使用synchronized和wait notify实现阻塞队列

我们先使用synchronized和wait notify来实现一个简单的阻塞队列。

// 自定义阻塞队列
@Slf4j
public class MyBolckQueue<T> {
    //使用LinkedList来作为存储内容的容器
    private LinkedList<T> list = new LinkedList<>();
    //队列的容量
    private int capcity;

    public MyBolckQueue(int capcity) {
        this.capcity = capcity;
    }

    //给队列中put元素的方法
    public void put(T element) {
        // 将list作为锁对象,先获取锁
        synchronized (list) {
            //队列如果满了,要让生产者线程阻塞等待
            while(list.size()==capcity) {
                try {
                    System.out.println("生产者等待");
                    list.wait();//当前线程进入等待状态,释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //走到这里表示队列里有空位了
            //从队尾取元素,从对头放元素,保证先入先出
            list.addFirst(element);
            log.info("生产者放{},成功",element);
            //唤醒在这个锁上等待的其他线程,是为了叫醒在等待的消费者,
            //这里叫醒后只是把哪些线程从waitSet转到了entryList,从Waiting变到Block状态,
            //只有等当前线程(Monitor的onwener)释放锁后哪些Block状态的线程才会开始竞争锁,
            // 并由是否抢到锁决定是否执行
            list.notifyAll();
        }
    }

    // 从队列中获取元素的方法
    public T take() {
        //先加锁
        synchronized (list) {
            //队列中没有元素时消费者线程阻塞等待
            while(list.size() == 0){
                try {
                    log.info("消费者等待");
                    list.wait();//当前线程进入等待状态,释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //走到这里表示队列中有元素了
            //先叫醒在锁上等待的其他线程,但它们并不会执行,因为当前线程还没有释放锁
            list.notifyAll();
            //从队尾取元素,从对头放元素,保证先入先出
            //注意一定要把list中的元素删除,不只是获取
            return list.removeLast();//当前线程执行结束后会释放锁
        }
    }

    public static void main(String[] args) {
        MyBolckQueue<Integer> queue = new MyBolckQueue<>(2);
        for (int i = 0; i < 3; i++) {
            int item = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    queue.put(item);
                }
            }).start();
        }

		
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Integer take = queue.take();
                    log.info("消费者获取到:"+take);
                }
            }).start();
        }
    }
}

2.2 使用ReentrantLock实现阻塞队列

ReentrantLock实现阻塞队列的方式利用的是ReentrantLock#awaitReentrantLock#signalAl方法,

因为多条件变量的特性put和take元素可以在不同的条件变量上等待。


import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

// 用ReentrantLock实现阻塞队列
@Slf4j
public class MyBlockQueue2<T> {

    private int capacity;//容量
    //队列容器
    private LinkedList<T> list = new LinkedList<>();

    public MyBlockQueue2(int capacity) {
        this.capacity = capacity;
    }

    private ReentrantLock lock = new ReentrantLock();

    //put元素时等待的条件变量
    private Condition putWait = lock.newCondition();

    //take元素时等待的条件变量
    private Condition takeWait = lock.newCondition();

    public void put(T element){
        try {
            //先加锁
            lock.lock();
            while (capacity == list.size()) {
                //队列满了,需要等待
                try {
                    log.info("put {},等待",element);
                    putWait.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //走到这里表示队列中有空位了
            //从对头添加元素,从队尾取元素
            list.addFirst(element);
            //叫醒在takeWait上阻塞的线程
            takeWait.signalAll();
        } finally {
            //保证一定会解锁
            lock.unlock();
        }
    }

    public T take(){
        try {
            lock.lock();
            while(list.size() == 0) {
                //队列中没元素,取的线程需要阻塞
                try {
                    log.info("获取等待");
                    takeWait.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //叫醒放元素阻塞的线程,被叫醒后进入阻塞状态,但获取不到锁也不会执行,只有此方法执行
            //完释放锁后才会执行
            putWait.signalAll();
            //走到这里表示队列中有元素了,取出队列中最后一个元素
            return list.removeLast();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyBlockQueue2<Integer> queue = new MyBlockQueue2<>(2);
        for (int i = 0; i < 3; i++) {
            int item = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    queue.put(item);
                }
            }).start();
        }

	   //三个消费者对三个生产者,
       //每个生产者也都能放进去任务,每个消费者都可以获取到任务,所有线程都可以正常结束。
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Integer take = queue.take();
                    log.info("消费者获取到:"+take);
                }
            }).start();
        }
    }
}

三、jdk中的阻塞队列

3.1 BlockingQueue

BlockingQueue 是jdk中提供的阻塞队列的顶层接口,其中定义了一个标准的阻塞队列都应该支持哪些方法。

下面表格给出了当这些方法不能执行成功时的处理方式,总共有四种,抛出异常,返回固定值(false),阻塞,带时限的阻塞

方法类型 抛出异常 返回特殊值 阻塞 带时限的阻塞
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine(查看) element() peek()

挑几个方法具体说明下含义

add(e):给队列添加元素,当队列的容量已满时就会抛出异常 IllegalStateException

remove(): 移除队列头部的一个元素,这个方法和offer不同的是当队列为空时会抛出异常NoSuchElementException

element() :返回队列头部的元素,但并不把元素从队列中删除,队列为空时会抛出NoSuchElementException ,

这个方法和peek不同的地方就是它会抛出异常

offer(e) : 给添加元素,如果成功返回true,队列满了就返回false,

poll:从头部取元素,如果成功返回true,队列为空就返回false,

put(e):从队列的尾部取元素(会从队列删除),如果队列满了就会阻塞当前put的线程

take:从队列头部取元素,如果队列为空也会阻塞当前线程

3.2 ArrayBlockingQueue

这是jdk提供的一个基于数组实现的有界限的阻塞队列,内部只有一把锁ReentrantLock,有两个条件变量分别用于存元素和取元素时阻塞线程,有两个指针分别用来存元素和取元素

简单看下其中的部分源码,重点看下put和take方法

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
	// 这是真正存储元素的数组,构造方法中会根据传入的容量初始化这个数组
	final Object[] items;
	//获取元素时的指针
	int takeIndex;
	//存元素的指针,初始值是0
	int putIndex;
	//队列元素计数
	int count;
	//锁
	final ReentrantLock lock;
	
	/** Condition for waiting takes */
	//取元素时阻塞线程用的条件变量
    private final Condition notEmpty;

    /** Condition for waiting puts */
    //存元素时阻塞线程用的条件变量
    private final Condition notFull;
    //构造方法,传入队列的容量
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    // fair:true表示创建的是公平锁,在锁中等待的线程被唤醒后会按照阻塞时间的长短来竞争锁,
    // false表示非公平锁,在锁中等待的线程被唤醒后自由竞争锁
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    
    //这是put元素的方法
    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 {
        	//放在finally保证一定会解锁
            lock.unlock();
        }
    }
    
    //队列中添加元素
    private void enqueue(E x) {
    	//把类变量赋值给局部变量这是为了提高访问速度
        final Object[] items = this.items;
        //把要put的元素x放在数组的putIndex下标处
        items[putIndex] = x;
        //存元素的下标自增1,下次put就可以往下一个位置放元素,
        //所以放元素是在数组的尾部添加元素
        //如果+1后超出了数组的容量限制就重置成0,表示已按顺序放到了队列的末尾,
        //按先入先出的原则肯定是0号位的元素会先被取走,所以下次就在0号位put
        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")
        //从数组中取元素,并把这个下标处赋值成null,takeIndex默认值是0
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        //取完后让takeIndex自增指向下次要取的下标,
        //如果超过了数组的容量就重新指向0,因为按先入先出原则取完最后一个就要回到起点准备下一
        //轮获取
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }
}

总结一下就是ArrayBlockingQueue是一个基于数组实现的有界队列,内部只有一把锁,存和取的操作不能同时进行。

3.2 LinkedBlockingQueue

这是一个基于链表实现的阻塞队列,创建时可以不指定容量,但本质上看还是一个有界队列,当你不指定容量是默认的容量是Integer.MAX_VALUE。因为基于链表实现,所以存和取操作的是不同的节点,所以内部有两把锁,存和取是可以同时进行的。

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
	//链表的节点类
	static class Node<E> {
        Node<E> next;

        Node(E x) { item = x; }
    }
    //队列容量
    private final int capacity;
    //用来计数当前队列的元素个数
    private final AtomicInteger count = new AtomicInteger();
    
    //指向链表的头节点,要注意的是链表的头节点不放元素,head.item == null,
    //这是为了处理在链表中只有一个元素时存线程和取线程同时操作这个节点的next属性引发线程安全问题,
    //这样设计后当链表中只有一个头节点时就认为当前队列是空的,让取线程阻塞,就可以避免上边的问题。
    transient Node<E> head;
	//指向链表的尾节点
    private transient Node<E> last;
    //取元素的锁
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 取元素时等待的条件变量 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 存元素的锁 */
    private final ReentrantLock putLock = new ReentrantLock();

    /** 存元素时等待的条件变量 */
    private final Condition notFull = putLock.newCondition();
    
    // 无参数的构造方法
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    //有参数构造方法
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        //链表的头节点不存元素,当链表中只有一个元素时就认为队列是空的
        last = head = new Node<E>(null);
    }
    
    //存元素的方法
    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        
        int c = -1;
        //创建一个新节点
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //加锁
        putLock.lockInterruptibly();
        try {
            //如果队列已经满了就让存的线程阻塞
            while (count.get() == capacity) {
                notFull.await();
            }
            //调用添加元素的方法
            enqueue(node);
            //容量自增
            c = count.getAndIncrement();
            //如果自增后发现容量还没到上限就唤醒阻塞的存元素线程,
            //这里就是和ArrayBlockingQueue不同的地方,存的线程也会去唤醒别的存的线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
        	//释放锁
            putLock.unlock();
        }
        //只有当c=0时才会去叫醒取元素的线程,因为c=0说明添加之前队列是空的,才有可能会有阻塞的
        //取元素线程
        if (c == 0)
        	//唤醒获取元素的线程
            signalNotEmpty();
    }
    
    //往链表的尾部添加元素
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    //取元素的方法
    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //加锁
        takeLock.lockInterruptibly();
        try {
        	//如果队列为空就阻塞
            while (count.get() == 0) {
                notEmpty.await();
            }
            //从队列的链表中取出头节点返回
            x = dequeue();
            //容量自减
            c = count.getAndDecrement();
            //减之前的容量比1大说明还有元素,可以唤醒别的取元素的线程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //如果取这个元素之前队列满了,那就可能会有阻塞的存元素线程,所以调用唤醒
        //存元素线程的方法
        if (c == capacity)
            signalNotFull();
        return x;
    }
    //从头节点取元素,注意并不是真的从头节点,因为头节点不存元素,所以应该是
    //取头节点的下一个节点
    private E dequeue() {
        Node<E> h = head;//头结点
        //获取头节点的下一个节点
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        //取头节点下一个节点的元素
        E x = first.item;
        //把头节点的下一个节点的内容清空,这样它就可以作为新的头节点,
        //要注意头节点始终不存内容
        first.item = null;
        return x;
    }
}

3.3 SynchronousQueue

这其实也是一个基于链表实现的阻塞队列,它没有使用锁,用cas来保证线程安全,它没有容量的概念,

它的特点是每一个put元素的线程都会对应一个take元素的线程,在take线程没取走前put线程会一直阻塞,

源码上有几句注释很重要

//不能执行peek方法,因为只有移除元素时元素才会存在(不移除put线程会一直阻塞)
You cannot peek at a synchronous queue because an element is only present when you try to remove it; 
//没有线程取元素就不能存元素
you cannot insert an element (using any method) unless another thread is trying to remove it; 

所以这个队列类似于让两个线程没有缓冲的直接交换数据,但实际是它内部也有链表/栈存在的。

它支持公平模式和非公平模式,

公平模式:先入先出,非公平模式:先入后出类似栈

public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
	//这个抽象类用来取元素或者存元素,
	//当前类中提供了基于队列和基于栈的两种实现,对应公平和非公平两种模式
	abstract static class Transferer<E> {
        abstract E transfer(E e, boolean timed, long nanos);
    }
    
    static final class TransferStack<E> extends Transferer<E>{
    	//省略
    }
    static final class TransferQueue<E> extends Transferer<E>{
    	//省略
    }
    
    //用来存取数据的transferer,在构造方法中初始化,put/take方法都是直接调用这个对象的方法
    private transient volatile Transferer<E> transferer;
    
    public SynchronousQueue() {
        this(false);
    }
    
    public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }
}