防止Java并发编程变成'两面三刀',试试BlockingQueue吧!

579 阅读10分钟

前言

  Java的并发编程中常常需要在多个线程之间共享数据。这时,线程之间的协调和通信就变得至关重要。而BlockingQueue就是Java中解决线程间通信问题的一种非常有用的工具。它是一个支持两个附加操作的队列:插入和删除。其中,插入操作在队列满时会阻塞等待空间变得可用,而删除操作在队列为空时会阻塞等待有数据可用。这种阻塞特性可以避免一些传统的线程通信问题,如死锁和饥饿等,因此广泛应用于Java的并发编程中。

阻塞队列介绍

队列数据结构

  队列是一种线性数据结构,它可以用来存储和管理一组数据元素。队列的特点是先进先出(First-In-First-Out, FIFO),也就是说,最先进入队列的元素最先被处理,而最后进入队列的元素最后被处理。队列可以用于许多实际应用,如任务调度、消息传递、缓存等。

  队列通常包含两个指针,一个指向队列的头部(Head),另一个指向队列的尾部(Tail)。新元素可以通过尾部插入队列(入队),而最老的元素可以通过头部删除队列(出队)。

队列.png

阻塞队列

  BlockingQueue是Java中的一个接口,它表示一个阻塞队列,即在队列为空时从队列中获取元素的操作将被阻塞,直到队列中有可用元素为止;而在队列已满时添加元素的操作将被阻塞,直到队列中有空闲位置为止。

  BlockingQueue接口扩展了Queue接口,添加了一些阻塞操作,包括put和take方法。put方法将一个元素放入队列中,如果队列已满,则阻塞直到队列中有空闲位置为止。take方法从队列中获取一个元素,如果队列为空,则阻塞直到队列中有可用元素为止。除了put和take方法,BlockingQueue还有其他的操作,例如offer、poll、peek等等,它们与Queue接口中的操作类似,但是在队列已满或为空时的行为不同。

image.png

ArrayBlockingQueue

  ArrayBlockingQueue是最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定容量大小,利用 ReentrantLock 实现线程安全。ArrayBlockingQueue可以用于实现数据缓存、限流、生产者-消费者模式等各种应用。在生产者-消费者模型中使用时,如果生产速度和消费速度基本匹配的情况下,使用ArrayBlockingQueue是个不错选择;如果生产速度远远大于消费速度,则会导致队列填满,大量生产线程被阻塞。

使用

public class ArrayBlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        // 创建一个容量为3的阻塞队列
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
        new Thread(()->{
            // 向队列中添加元素
            try {
                queue.put("a");
                queue.put("b");
                queue.put("c");
                System.out.println("队列中当前元素个数:" + queue.size());
                // 尝试向队列中添加一个元素,但是队列已满,将会被阻塞
                queue.put("d");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }).start();


        Thread.sleep(500);
        // 从队列中获取元素
        String element = queue.take();
        System.out.println("获取到的元素为:" + element);
        System.out.println("队列中当前元素个数:" + queue.size());
        // 遍历队列中的元素
        System.out.print("队列中的元素为:");
        for (String s : queue) {
            System.out.print(s + " ");
        }
        System.out.println();
    }
}

输出结果

队列中当前元素个数:3
获取到的元素为:a
队列中当前元素个数:3
队列中的元素为:b c d 

  主线程和另一个线程并发地对阻塞队列进行读写操作。在另一个线程中,我们使用了put()方法向队列中添加元素,这是一个阻塞方法,如果队列已满,它将阻塞当前线程,直到队列中有空闲位置为止。因此,在另一个线程中,当我们向队列中添加第四个元素时,由于队列已满,put()方法将一直阻塞直到队列有空闲位置。而在主线程中,我们在等待500毫秒后,从队列中取出了一个元素,此时队列中只有三个元素,因此我们成功从队列中获取了一个元素。在输出队列中当前元素个数和遍历队列中的元素时,我们看到只有三个元素,这也验证了我们的推断。

源码解读

数据结构

//数据元素数组
final Object[] items;

//下一个待取出的元素下标索引
int takeIndex;

//下一个待放入的元素下标索引
int putIndex;

//元素个数
int count;

//内部锁
final ReentrantLock lock;

//消费者条件
private final Condition notEmpty;

//生产者条件
private final Condition notFull;

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 {
        lock.unlock();
    }
}
private void enqueue(E x) {
    final Object[] items = this.items;
    //入队
    items[putIndex] = x;
    //putIndex进行自增(入队索引)
    if (++putIndex == items.length)
        putIndex = 0; //环形数组,putIndex指针到数组尽头了,返回头部
    count++;
    //到这里入队成功队列不为空去唤醒消费者去消费
    notEmpty.signal();
}

take()

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; //环形数组 消费到队尾要重置到0从对头重新开始消费
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    //此时队列有空位,唤醒生产者继续生产消息
    notFull.signal();
    return x;
}

LinkedBlockingQueue

  LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小。

  LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素,LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。

使用

public class LinkedBlockingQueueDemo {

    public static void main(String[] args) {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(3); //创建一个容量为3的阻塞队列
        // 启动一个生产者线程
        new Thread(new Producer(queue)).start();
        // 启动一个消费者线程
        new Thread(new Consumer(queue)).start();
    }

    // 生产者线程
    static class Producer implements Runnable {
        private final LinkedBlockingQueue<String> queue;

        public Producer(LinkedBlockingQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                // 不断向队列中添加元素
                for (int i = 1; i <= 5; i++) {
                    String element = "element" + i;
                    queue.put(element); // put方法会阻塞线程,直到队列中有空闲位置为止
                    System.out.println("生产者生产了:" + element);
                    Thread.sleep(1000); // 模拟生产过程中的耗时操作
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 消费者线程
    static class Consumer implements Runnable {
        private final LinkedBlockingQueue<String> queue;

        public Consumer(LinkedBlockingQueue<String> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                // 不断从队列中取出元素进行消费
                while (true) {
                    String element = queue.take(); // take方法会阻塞线程,直到队列中有可用元素为止
                    System.out.println("消费者消费了:" + element);
                    Thread.sleep(2000); // 模拟消费过程中的耗时操作
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  该示例中,创建了一个容量为3的LinkedBlockingQueue,然后启动一个生产者线程和一个消费者线程,生产者线程向队列中添加元素,消费者线程从队列中取出元素进行消费。由于LinkedBlockingQueue是一个阻塞队列,因此在队列已满或为空时,生产者和消费者线程都会被阻塞,直到队列中有空闲位置或有可用元素为止。

源码解读

数据结构

//容量
private final int capacity;

//元素数量
private final AtomicInteger count = new AtomicInteger();

/**
 * 链表头节点
 * Invariant: head.item == null 初始化时为null
 */
transient Node<E> head;

/**
 * 链表尾节点
 * Invariant: last.next == null 初始化时为null
 */
private transient Node<E> last;

// take锁
private final ReentrantLock takeLock = new ReentrantLock();

//当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = takeLock.newCondition();

//put锁
private final ReentrantLock putLock = new ReentrantLock();

//当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();

put()

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    //新建节点
    Node<E> node = new Node<E>(e);
    //获取put锁
    final ReentrantLock putLock = this.putLock;
    //获取元素数量
    final AtomicInteger count = this.count;
    //加锁
    putLock.lockInterruptibly();
    try {
        //元素数量和容量相等时,进入阻塞等待消费者唤醒
        while (count.get() == capacity) {
            notFull.await();
        }
        //入队
        enqueue(node);
        //队列长度+1
        c = count.getAndIncrement();
       //如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可继续入队)
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        putLock.unlock();
    }
    if (c == 0)
    // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
        signalNotEmpty();
}
private void enqueue(Node<E> node) {
   //直接加到last后面,last指向入队元素
    last = last.next = node;
}

take()

public E take() throws InterruptedException {
    E x;
    int c = -1;
     //获取元素数量
    final AtomicInteger count = this.count;
     //获取take锁
    final ReentrantLock takeLock = this.takeLock;
    //加锁
    takeLock.lockInterruptibly();
    try {
        //元素数量为0时当前线程阻塞,等待唤醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        //出队
        x = dequeue();
        //长度减一返回原值
        c = count.getAndDecrement();
        //如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    //如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
    if (c == capacity)
        signalNotFull();
    return x;
}
private E dequeue() {
    // head节点本身是不存储任何元素的
    // 这里把head删除,并把head下一个节点作为新的值
    // 并把其值置空,返回原来的值
    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;
}

DelayQueue

  DelayQueue是Java中的一个并发队列实现,它是PriorityQueue的一个特殊变体,用于存储实现了Delayed接口的元素。Delayed接口定义了一个getDelay()方法,该方法返回元素需要延迟的时间,以及一个compareTo()方法,该方法用于比较元素之间的顺序。

使用

public class DelayQueueDemo {

    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();

        // 创建两个任务,一个延迟5秒,一个延迟10秒
        DelayedTask task1 = new DelayedTask("task1", 5, TimeUnit.SECONDS);
        DelayedTask task2 = new DelayedTask("task2", 10, TimeUnit.SECONDS);

        // 将任务加入DelayQueue
        delayQueue.put(task1);
        delayQueue.put(task2);

        // 循环获取任务并执行
        while (!delayQueue.isEmpty()) {
            DelayedTask task = delayQueue.take();
            System.out.println("execute task: " + task.getName());
        }
    }

    // 实现Delayed接口的任务类
    static class DelayedTask implements Delayed {
        private final String name;
        private final long delayTime;
        private final long expireTime;

        public DelayedTask(String name, long delay, TimeUnit unit) {
            this.name = name;
            this.delayTime = TimeUnit.MILLISECONDS.convert(delay, unit);
            this.expireTime = System.currentTimeMillis() + this.delayTime;
        }

        public String getName() {
            return name;
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(Delayed o) {
            return Long.compare(this.expireTime, ((DelayedTask) o).expireTime);
        }
    }
}

输出结果

execute task: task1
execute task: task2

  上面的代码创建了一个DelayQueue,并向其中加入两个任务,一个延迟5秒,一个延迟10秒。然后循环从DelayQueue中获取任务并执行,因为第一个任务要延迟5秒执行,所以先输出执行第一个任务的信息,等5秒后再执行第二个任务。

源码解读

数据结构

//内部锁
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列,存储元素,用于保证延迟低的优先执行
private final PriorityQueue<E> q = new PriorityQueue<E>();
//用于标记当前是否有线程在排队
private Thread leader = null;
//用于表示现在是否有可取的元素条件
private final Condition available = lock.newCondition();

put()

public void put(E e) {
    offer(e);
}
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        //入队
        q.offer(e);
        // 若入队的元素位于队列头部,说明当前元素延迟最小
        if (q.peek() == e) {
            //将leader置为空
            leader = null;
            //唤醒阻塞在available上的线程
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}
这是调用了优先级队列的的入队方法
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    // 获取当前队列的元素个数
    int i = size;
    // 如果队列已满,则扩容
    if (i >= queue.length)
        grow(i + 1);
    // 元素个数加1
    size = i + 1;
    // 如果队列为空,直接将元素放入队列第一个位置
    if (i == 0)
        queue[0] = e;
    else
        // 如果队列不为空,将元素插入到合适的位置
        siftUp(i, e);
    return true;
}

take()

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lockInterruptibly();
    try {
       //死循环等待主动跳出
        for (;;) {
            //取最早过期的元素
            E first = q.peek();
            //为空就阻塞等待数据到来
            if (first == null)
                available.await();
            else {
                //获取到期时间
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return q.poll();//到期就弹出该元素
                first = null; // don't retain ref while waiting
                if (leader != null)
                    //如果有线程争抢的Leader线程,则进行无限期等待
                    available.await();
                else {
                    //leader为空就把当前线程复制给leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        //等待剩余等待时间
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                           //释放leader
                            leader = null;
                    }
                }
            }
        }
    } finally {
        //leader为空同时有下一个元素则释放凭证唤醒
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}