java多线程 ArrayBlockingQueue源码分析

目录

前言

成员

构造器

入队

add

offer

put

超时offer

总结

出队

peek

poll

take

超时poll

总结

remove 删除操作

总结


注意:本文转自   https://blog.csdn.net/anlian523/article/details/107577452

前言

ArrayBlockingQueue是一种FIFO(first-in-first-out 先入先出)的有界阻塞队列,底层是数组,也支持从内部删除元素。并发操作依赖于加锁的控制,支持阻塞式的入队出队操作。正因为有界,所以才会阻塞。

加锁实现完全依赖于AQS,需要读者比较熟悉AQS 独占锁的获取过程和AQS Condition接口的实现。对ArrayBlockingQueue的源码解析,更像是了解一次AQS的最佳实践。

成员

    //保存队列元素的数组
    final Object[] items;

    //下次出队的位置
    int takeIndex;

    //下次入队的位置
    int putIndex;

    //队列中元素的数量
    int count;

	final ReentrantLock lock;
	private final Condition notEmpty;
	private final Condition notFull;

队列中非null元素的范围是[takeIndex, putIndex)的左闭右开的区间。考虑到底层是循环数组,有可能putIndex比takeIndex小。二者相等也很好理解,代表队列中每个元素都是非null元素。

术语:

队列:指ArrayBlockingQueue本身。

同步队列:指AQS的sync queue。

条件队列:指AQS的condition queue。

构造器

    public ArrayBlockingQueue(int capacity) {
        this(capacity, 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();
    }

构造器默认使用的是非公平的ReentrantLock,当然你也可以指定为公平的ReentrantLock。

    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // 加锁只是为了保证可见性
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;//如果传入集合的个数超过了容量,抛出异常被catch,最多放capacity个
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;//循环结束,i刚好是放置的个数
            putIndex = (i == capacity) ? 0 : i;//循环结束,i也刚好是最后放置元素的索引+1
        } finally {
            lock.unlock();
        }
    }

如果传入集合的个数超过了容量,抛出异常被catch,最多放capacity个元素。

入队

add

//ArrayBlockingQueue.java
    public boolean add(E e) {
        return super.add(e);
    }
    
//AbstractQueue.java
    public boolean add(E e) {
        if (offer(e))
            return true;
        else//返回false的处理不一样
            throw new IllegalStateException("Queue full");
    }
    
//Queue.java(接口文件)
	boolean offer(E e);

add的实现是依靠父类的add实现,后者又依靠于子类的offer实现。所以,add就是在调用自己的offer方法,只不过有点绕。

offer

    private static void checkNotNull(Object v) {
        if (v == null)
            throw new NullPointerException();
    }

    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();
        }
    }
  • 入队是一个写操作,自然需要加锁。lock.lock()不响应中断,线程会一直阻塞直到抢到锁。
  • 队列已满,则无法入队,返回false。
  • 队列未满,则可以入队,返回true。
    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
            putIndex = 0;
        count++;//大小加1
        notEmpty.signal();//队列不为空的条件,已经满足。
    }
  • 在putIndex位置是空的,我们直接往putIndex索引上入队。
  • 右移putIndex,按照循环数组的方式。
  • 队列大小加1。
  • 通知沉睡在notEmpty条件队列上的线程,只通知一个线程。

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

在进入加锁代码之前,执行的是lock.lockInterruptibly()。这意味着,当前线程在抢到锁之前,如果被中断了,put方法会抛出中断异常。

进入加锁代码之后,当前线程便已是获得了锁。但获得了锁,和队列当前是空是满根本没有关系。

如果队列未满,那么根本不会执行notFull.await(),直接入队。

需要使用while (count == items.length)来防止虚假唤醒,即使当前线程从notFull.await()恢复执行了,如果当前队列还是满的,那么应该重新进入条件队列。所以,需要重新检查一遍count == items.length。

你可能会产生疑问,为什么需要重新检查一遍。因为当前线程从notFull.await()恢复执行,一定是因为别的线程执行了notFull.signal()(别的线程的这个时间点,队列确实未满)。但由于当前线程是从AQS的条件队列转移到AQS的同步队列的队尾,而排在同步队列前面的其他线程也有可能去执行入队操作,可能等到当前线程获得锁后(所以才会从notFull.await()恢复执行),队列又变成满了。

此put函数只有成功入队后,才可能从put调用处返回。

当队列未满,则入队。

超时offer

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

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)//如果队列是满的,且等待时间<= 0这代表不用等待,所以直接返回false
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

相比上一个实现,使用的是awaitNanos。

  • 从notFull.awaitNanos(nanos)返回有三种原因:超时前的signal、超时前的中断、超时。
  • 超时前的signal。只有这种情况,才可能返回一个大于0的数字。
  • 超时前的中断。返回时,抛出中断异常。
  • 超时(不管之后有没有中断)。只可能返回一个小于0的数字。
  • 因为超时前的signal而从notFull.awaitNanos(nanos)返回,需要进行虚假唤醒的检查。如果此时队列还是满的,当前线程再次进入AQS的条件队列;如果此时队列确实未满,那么入队,返回true。
  • 如果此时队列是满的,当前线程再次进入AQS的条件队列之前,需要检查剩余时间是否大于0,如果不是大于0,说明在awaitNanos上话费的时间已经超过了限制,则返回false。

某种情景再现:

  • 当前线程调用notFull.awaitNanos(500),准备进行500ns的等待。
  • 别的线程在剩余时间大约还有300ns的时间时,调用了notFull.signal(),唤醒了当前线程。
  • 当前线程从notFull.awaitNanos(500)处返回,返回值为300。
  • 循环继续,检查却发现队列已满。
  • if (nanos <= 0)不满足,继续执行notFull.awaitNanos(300)。
  • 当前线程继续等待300ns。

总结

出队

peek

    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

直接返回索引处元素,可能为null(队列为空),正如peek的含义,只获取不出队。

poll

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    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,按照循环数组的方式
            takeIndex = 0;
        count--;//大小减1
        if (itrs != null)
            itrs.elementDequeued();//迭代器必要操作
        notFull.signal();//通知阻塞在notFull条件队列上的第一个线程
        return x;
    }

此函数可能返回null,当队列为空时。

take

此函数与put相对应。实现与put完全对称,好像没什么好讲的。

    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相对应。实现与超时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)//如果队列是空的,且等待时间<= 0这代表不用等待,所以直接返回null
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();//如果队列确实不空,那么执行出队动作
        } finally {
            lock.unlock();
        }
    }

总结

remove 删除操作

该函数如果删除的不是队首元素,会涉及到整体移动的过程,可能会比较耗时,不建议使用。

现在队列中非null元素的范围是[takeIndex, putIndex)的左闭右开的区间。

    public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {//队列有元素存在
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
                //到达区间[takeIndex, putIndex)的边界,说明所有非null元素都找遍了
            }
            return false;//没有找到元素
        } finally {
            lock.unlock();
        }
    }

循环从[takeIndex, putIndex)的左边界开始,直到右边界结束。如果找到元素,则删除它。

    void removeAt(final int removeIndex) {
        // assert lock.getHoldCount() == 1;
        // assert items[removeIndex] != null;
        // assert removeIndex >= 0 && removeIndex < items.length;
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {//如果刚好删除的是队首,那刚好是一个出队动作
            // removing front item; just advance
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {//其他情况
            //[i,putIndex)区间内的第一个元素被删除,需要往左压实这个区间
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {//还没到达边界
                    items[i] = items[next];//将后面的复制到前面去
                    i = next;
                } else {//到达边界
                    items[i] = null;//清空区间内最后一个元素
                    this.putIndex = i;//最后putIndex当然也得左移,i此时肯定是putIndex - 1
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        notFull.signal();
    }
  • 如果刚好删除的队首元素,那刚好是一次出队操作。
  • 如果是其他情况,现在删除的是i索引元素,但为了队列非null元素连续(考虑循环数组也得连续),那么[i, putIndex)区间内的第一个元素已经被删除变成null了,需要往左压实,即[i+1, putIndex)内的元素整体左移。

总结

  • 当队列为空或为满时,takeIndex putIndex二者才会相同。
  • 所有常用操作都需要加锁,甚至是属于读操作的peek,因为加锁强制内存刷新,能让线程看到最新的队列。
  • 入队出队操作,都有一次尝试版本,和阻塞等待版本。
  • 使用Lock来控制并发操作。
  • 两个Condition的使用,是控制阻塞等待的关键。
  • 删除操作支持删除内部元素。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值