线程同步利器-阻塞队列

532 阅读4分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

核心原理

image.png

  1. 基于数组实现的阻塞队列中有两个游标,一个是TakeIndex代表下一个任务的位置,和putIntex代表下一个存放任务的位置

  2. 投放和获取任务时都需要先获取到同步锁,然后才能对阻塞队列进行操作,如果投放任务时,发下阻塞队列已经满了,那么需要根据调用的api特点决定将投放线程挂起还是直接返回投放失败。获取任务也是同样的道理

  3. 数组收尾相连构成循环数组,这样就可以将数组作为一个环状的缓冲区使用。

阻塞队列核心API介绍

添加任务到阻塞队列

  1. boolean add(E e) 添加任务成功,返回true,队列满,抛出 IllegalStateException异常

  2. boolean offer(E e) 添加任务成功,返回true,队列满,添加失败,返回false

  3. void put(E e) throws InterruptedException 添加任务到阻塞队列,如果队列满了,会阻塞提交任务的线程,直到将任务添加到队列中

  4. boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 添加任务到队列,并可以指定最大等待时间,如果超过最大等待时间,那么返回false

从阻塞队列中获取任务

  1. E take() throws InterruptedException; 从队列头部获取任务,如果队列为空,挂起获取任务的线程,知道线程获取到任务

  2. E poll(long timeout, TimeUnit unit)throws InterruptedException; 从队列头部获取任务,并可以指定最大等待时间,如果超过等待时间还没有获取任务,那么返回null

ArrayBlockQueue

核心属性

public class ArrayBlockingQueue<E> 
        extends AbstractQueue<E> 
        implements BlockingQueue<E>, java.io.Serializable {

    /**
     * 基于数组实现的队列
     */
    final Object[] items;

    /**
     * 获取下一个任务的下标
     */
    int takeIndex;

    /**
     * 下一个存放任务的位置
     */
    int putIndex;

    /**
     * 队列中任务的数量
     */
    int count;

    /**
     * 互斥锁
     */
    final ReentrantLock lock;

    /**
     * 队列为空时,等待获取任务的线程监视器
     */
    private final Condition notEmpty;

    /**
     * 队列满时,等待投放任务的线程监视器
     */
    private final Condition notFull;
}

非阻塞投放任务

add方法只是对offer方法做了一个封装,调用offer方法后,如果返回true直接返回,否则抛出IllegalStateException异常

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

offer方法是真正进行任务投放的方法,首先尝试获取lock同步锁,如果,获取成功后,判断队列中任务是否已经满了, 如果此时队列中任务已经满了,直接返回false,否则调用enqueue进行入队操作。

    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
任务入队

直接将任务放置到数组下表为putIndex的位置,并对putIndex执行+1操作,然后判断是否超过数组的大小, 如果超过,直接将putIndex的值重置为0,此时任务已经添加成功,所以,队列中任务的count需要执行count++操作。 最后一步是唤醒被notEmpty监视器监视的一个线程,因为可能之前阻塞队列为空,导致想获取阻塞队列任务的线程被挂起。所以此时需要进行唤醒。

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

阻塞获取任务

尝试获取锁,获取成功后,判断队列是否有空间,如果空间,继续执行挂起,如果有空间,那么执行入队操作。

    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();
        }
    }

从队列中获取任务

尝试获取同步锁,如果获取成功,判断队列中是否有元素,如果没有,挂起线程,如果有,执行dequeue方法进行出队操作。

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
任务出队

直接通过takeIndex获取任务,这里有一个细节就是获取完任务后,执行了items[takeIndex] = null,这样可以让对象及时的被回收,避免内存泄露问题。 获取完任务后,相应的对count进行了--操作,因为可能存在投放任务但是队列满而导致有些投放任务的线程被挂起,所以这里最后还需要执行唤醒操作。

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        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;
    }

带超时时间的获取任务

这个方法本质上和take方法没有区别,awaitNanos(nanos)方法是休眠指定的时间,如果休眠过程中被唤醒,那么返回剩余没有休眠的时间,程序通过这个方法确保我们的线程可以休眠到最大的等待时间。

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {
            if (nanos <= 0)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}