最简单的同步队列--ArrayBlockingQueue

ArrayBlockingQueue是一个有界的阻塞队列,使用单锁双条件实现,保证线程安全。入队通过put方法进行,出队使用take方法。构造器中定义数组大小,当队列满时,put方法会阻塞,而非阻塞的offer方法可设置超时时间。在队列空时,take方法会阻塞,保证同步操作。ArrayBlockingQueue相比传统的同步容器性能有所提升,使用1.5引入的Lock机制。

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

ArrayBlockingQueue

关键词

阻塞
有界
单锁双条件

    final Object[] items;//底层数组
    int takeIndex; //出队索引,由lock保证线程安全性
    int putIndex;//入队索引,由lock保证线程安全性
    int count;
    /*
     * Concurrency control uses the classic two-condition algorithm     */
    final ReentrantLock lock; //锁对象,AQS实现
    private final Condition notEmpty;//Object wait和notify的替代品
    private final Condition notFull;//Object wait和notify的替代品

构造器

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

定义数组大小,所以有界。只有一把重入锁和这把锁上的两个condition,notEmpty控制队列为空时线程的阻塞和唤醒,notFull 控制队列满时线程的阻塞和唤醒。

入队

先看enqueue方法,他在offer,put方法用来入队。可以看到这个方法入队操作非常简单,第5行直接将元素填入数组,该方法没有加锁,入队之后putIndex加一,并唤醒因为队列空而阻塞的线程。为什么没加锁留到put方法说明。

1    private void enqueue(E x) {
2        // assert lock.getHoldCount() == 1;
3        // assert items[putIndex] == null;
4        final Object[] items = this.items;
5        items[putIndex] = x;
6        if (++putIndex == items.length)
7            putIndex = 0;
8        count++;
9        notEmpty.signal();
    } 
  • put方法

由上一篇BlockingQueue的介绍我们知道,put方法必须入队,当时猜想的put方法是要用循环来保证入队,这里也确实如此,只不过把队满作为了循环条件。同步方面,ArrayBlockingQueue用一把锁保证了入队和出队操作同一时刻只能有一个发生。put方法本身已经加锁了,所以enqueue()方法自然也就不用加锁了。
另外:当队列满的时候,await()会释放锁,否则入队出队公用一把锁,poll,take等方法也拿不到锁,会造成死锁。
如果遇到什么情况会死锁这一面试题时,可以回答:同步队列用同一把锁的时候,入队线程持有锁却在等待出队线程出队。这是典型的A在等待B的操作,B又在等待A释放锁。
使用单线程池时,若任务之间有相互关系,也会造成这一情况。

    public void put(E e) throws InterruptedException {
        checkNotNull(e); //检查e不为空
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();//这里会释放锁,再次唤醒时会获取锁
            enqueue(e);//该方法不需要加锁,代码已经实现了同一时刻该方法只会被一个线程调用,且同一时刻不会有出队操作。
        } finally {
            lock.unlock();
        }
    }
  • offer方法

该方法相比于put方法,唯一区别就是在等待的时候调用了awaitNanos(time),而不是await(),等同于把计算等待多长时间的操作放到了AbstractQueueSynchronizer中完成,AQS实现中断能够定时唤醒用的还是LockSupport.parkNanos(time),LockSupport用的是UNSAFE.park(false, nanos);UNSAFE用的是本地方法,所以这个操作并不是JAVA代码完成的。
另外看过接口介绍的同学应该能够明白为啥offer要返回true false 而put不需要了吧。

    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);//检查e不为空
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

Unsafe类中的park方法在这里插入图片描述

出队

  • take方法
    先看dequeue方法,与enqueue相对应,这个方法也没有加锁,同步逻辑同样是在调用它的take,poll方法来完成的。与插入不同的是,这里引入了一个 itrs.elementDequeued的方法,这是删除迭代器中该元素的操作。我们知道非同步容器ArrayList等在容器变化的时候,迭代器需要抛出ConcurrentModificationException,并发容器么有这一需要,所以elementDequeued方法会把迭代过程中的移除的元素去掉。
    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;
   }

与put方法相对应的take方法,while循环直到拿到元素。

   public E take() throws InterruptedException {
       final ReentrantLock lock = this.lock;
       lock.lockInterruptibly();
       try {
           while (count == 0)
               notEmpty.await(); //这里会释放锁
           return dequeue();//出队
       } finally {
           lock.unlock();
       }
   }
  • poll方法
    与offer方法相对应,逻辑基本一样,这里就不展开了。
    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;//到时间仍然未获取到元素则返回null。
               nanos = notEmpty.awaitNanos(nanos);//释放锁等待固定时长,期间若被唤醒,会获取锁继续循环判断是否有元素可以出队。
           }
           return dequeue();
       } finally {
           lock.unlock();
       }
   }

其他相关方法就不展开了,大家可以自己看源码。

ArrayBlockingQueue可以说是java中最早出现的同步队列了,相比于Collections.synchronizedCollection()的同步快包装实现,它使用了1.5引入的细粒度锁lock,较之以前的synchronize同步块提升了一定的性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值