[Java 基础]ArrayBlockingQueue源码解析

本文参考源码版本是1.8。

ArrayBlockingQueue.png

ArrayBlockingQueue是由数组支持的有界阻塞队列。它的本质是一个基于数组的blocking queue的实现。
它的容纳大小是固定的。此队列按 FIFO(先进先出)原则对元素进行排序。
队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。
新元素插入到队列的尾部,队列检索操作则是从队列头部开始获得元素。
这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。
一旦创建了这样的缓存区,就不能再增加其容量。
试图向已满队列中放入元素会导致放入操作受阻塞,直到BlockingQueue里有新的空间才会被唤醒继续操作;
试图从空队列中检索元素将导致类似阻塞,直到BlocingkQueue进了新货才会被唤醒。
此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。
默认情况下,不保证是这种排序。然而,通过在构造函数将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。
公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
此类及其迭代器实现了 Collection 和 Iterator 接口的所有可选 方法。
注意1:它是有界阻塞队列。它是数组实现的,是一个典型的“有界缓存区”。数组大小在构造函数指定,而且从此以后不可改变。
注意2:是它线程安全的,是阻塞的,具体参考BlockingQueue的“注意4”。
注意3:不接受 null 元素
注意4:公平性 (fairness)可以在构造函数中指定。

1、对于ArrayBlockingQueue需要掌握以下几点

创建
入队(添加元素)
出队(删除元素)
2、创建

public ArrayBlockingQueue(int capacity, boolean fair)
public ArrayBlockingQueue(int capacity)
使用方法:

Queue<String> abq = new ArrayBlockingQueue<String>(2);
Queue<String> abq = new ArrayBlockingQueue<String>(2,true);
通过使用方法,可以看出ArrayBlockingQueue支持ReentrantLock的公平锁模式与非公平锁模式,对于这两种模式,查看本文开头的文章即可。

源代码如下:

    private final E[] items;//底层数据结构
    private int takeIndex;//用来为下一个take/poll/remove的索引(出队)
    private int putIndex;//用来为下一个put/offer/add的索引(入队)
    private int count;//队列中元素的个数

    /*
     * Concurrency control uses the classic two-condition algorithm found in any
     * textbook.
     */

    /** Main lock guarding all access */
    private final ReentrantLock lock;//锁
    /** Condition for waiting takes */
    private final Condition notEmpty;//等待出队的条件
    /** Condition for waiting puts */
    private final Condition notFull;//等待入队的条件

 /**
     * 创造一个队列,指定队列容量,指定模式
     * @param fair
     * true:先来的线程先操作
     * false:顺序随机
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = (E[]) new Object[capacity];//初始化类变量数组items
        lock = new ReentrantLock(fair);//初始化类变量锁lock
        notEmpty = lock.newCondition();//初始化类变量notEmpty Condition
        notFull = lock.newCondition();//初始化类变量notFull Condition
    }

    /**
     * 创造一个队列,指定队列容量,默认模式为非公平模式
     * @param capacity <1会抛异常
     */
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

注意:
ArrayBlockingQueue的组成:一个对象数组、1把锁ReentrantLock、2个条件Condition
在查看源码的过程中,也要模仿带条件锁的使用,这个双条件锁模式是很经典的模式

3、入队

3.1、public boolean offer(E e)

原理:

在队尾插入一个元素, 如果队列没满,立即返回true; 如果队列满了,立即返回false
使用方法:

abq.offer("hello1");
源代码:

 /**
     * 在队尾插入一个元素,
     * 如果队列没满,立即返回true;
     * 如果队列满了,立即返回false
     * 注意:该方法通常优于add(),因为add()失败直接抛异常
     */
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();
        }
    }

    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    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)// 移动数组下标+1
            putIndex = 0;
        count++;//元素数量+1

/**         * 唤醒一个线程
         * 如果有任意一个线程正在等待这个条件,那么选中其中的一个区唤醒。
         * 在从等待状态被唤醒之前,被选中的线程必须重新获得锁
         */
        notEmpty.signal();
    }

代码非常简单,流程看注释即可,只有一点注意点:

在插入元素结束后,唤醒等待notEmpty条件(即获取元素)的线程,可以发现这类似于生产者-消费者模式

因为队列是定长的,不会自耦定扩容,为了提高效率,会循环使用数组,所以对头不一定是0。总结章节会详细说明元素移动过程。

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

原理:

在队尾插入一个元素,,如果数组已满,则进入等待,直到出现以下三种情况:
被唤醒
等待时间超时
当前线程被中断
使用方法:

 try {
            abq.offer("hello2",1000,TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

源代码:

    /**
     * 在队尾插入一个元素,
     * 如果数组已满,则进入等待,直到出现以下三种情况:
     * 1、被唤醒
     * 2、等待时间超时
     * 3、当前线程被中断
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);//将超时时间转换为纳秒
        final ReentrantLock lock = this.lock;
/*
         * lockInterruptibly():
         * 1、 在当前线程没有被中断的情况下获取锁。
         * 2、如果获取成功,方法结束。
         * 3、如果锁无法获取,当前线程被阻塞,直到下面情况发生:
         * 1)当前线程(被唤醒后)成功获取锁
         * 2)当前线程被其他线程中断
         * 
         * lock()
         * 获取锁,如果锁无法获取,当前线程被阻塞,直到锁可以获取并获取成功为止。
         */
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)//已超时
                    return false;
                   /*
                     * 进行等待:
                     * 在这个过程中可能发生三件事:
                     * 1、被唤醒-->继续当前这个for(;;)循环
                     * 2、超时-->继续当前这个for(;;)循环
                     * 3、被中断-->之后直接执行catch部分的代码
                     */
                nanos = notFull.awaitNanos(nanos);//进行等待(在此过程中,时间会流失,在此过程中,线程也可能被唤醒)
            }
          //队列未满
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

注意:

awaitNanos(nanos)是AQS中的一个方法,这里就不详细说了,有兴趣的自己去查看AQS的源代码。
lockInterruptibly()与lock()的区别见注释

3.3、public void put(E e) throws InterruptedException

原理:

在队尾插入一个元素,如果队列满了,一直阻塞,直到数组不满了或者线程被中断
使用方法:

  try {
            abq.put("hello1");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

源代码:

/**
     * 在队尾插入一个元素
     * 如果队列满了,一直阻塞,直到数组不满了或者线程被中断
     */
   public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)//队列满了,一直阻塞在这里
                     /*
                     * 一直等待条件notFull,即被其他线程唤醒
                     * (唤醒其实就是,有线程将一个元素出队了,然后调用notFull.signal()唤醒其他等待这个条件的线程,同时队列也不慢了)
                     */
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

4、出队

4.1、public E poll()

原理:

如果没有元素,直接返回null;如果有元素,将队头元素置null,但是要注意队头是随时变化的,并非一直是items[0]。
使用方法:

abq.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;//将出队元素位置置空

        /*
         * 第一次出队的元素takeIndex==0,第二次出队的元素takeIndex==1
         * (注意:这里出队之后,并没有将后面的数组元素向前移)
         */
        if (++takeIndex == items.length)
            takeIndex = 0;

        count--;数组元素个数-1
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();//数组已经不满了,唤醒其他等待notFull条件的线程
        return x;//返回出队的元素
    }

4.2、public E poll(long timeout, TimeUnit unit) throws InterruptedException

原理:

从对头删除一个元素,如果数组不空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
被唤醒
等待时间超时
当前线程被中断
使用方法:

 try {
            abq.poll(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

源代码:

   /**
     * 从对头删除一个元素,
     * 如果数组不空,出队;
     * 如果数组已空,判断时间是否超时,如果已经超时,返回null
     * 如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:
     * 1、被唤醒
     * 2、等待时间超时
     * 3、当前线程被中断
     */
    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;
                 /*
                     * 进行等待:
                     * 在这个过程中可能发生三件事:
                     * 1、被唤醒-->继续当前这个for(;;)循环
                     * 2、超时-->继续当前这个for(;;)循环
                     * 3、被中断-->之后直接执行catch部分的代码
                     */
                nanos = notEmpty.awaitNanos(nanos);
            }
            //数组不空
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

4.3、public E take() throws InterruptedException

原理:

将队头元素出队,如果队列空了,一直阻塞,直到数组不为空或者线程被中断
使用方法:

 try {
            abq.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

源代码:

    /**
     * 将队头元素出队
     * 如果队列空了,一直阻塞,直到数组不为空或者线程被中断
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)//如果数组为空,一直阻塞在这里
                /*
                     * 一直等待条件notEmpty,即被其他线程唤醒
                     * (唤醒其实就是,有线程将一个元素入队了,然后调用notEmpty.signal()唤醒其他等待这个条件的线程,同时队列也不空了)
                     */
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

总结

1、具体入队与出队的原理图:这里只说一种情况,见下图,途中深色部分表示已有元素,浅色部分没有元素。

上面这种情况是怎么形成的呢?当队列满了,这时候,队头元素为items[0]出队了,就形成上边的这种情况。
假设现在又要出队了,则现在的队头元素是items[1],出队后就形成下面的情形。


出队后,对头元素就是items[2]了,假设现在有一个元素将要入队,根据inc方法,我们可以得知,他要插入到items[0]去,入队了形成下图:



以上就是整个入队出队的流程,inc方法上边已经给出,这里再贴一遍:

        if (++putIndex == items.length)
            putIndex = 0;

2、三种入队对比:

offer(E e):如果队列没满,立即返回true; 如果队列满了,立即返回false-->不阻塞
put(E e):如果队列满了,一直阻塞,直到数组不满了或者线程被中断-->阻塞
offer(E e, long timeout, TimeUnit unit):在队尾插入一个元素,,如果数组已满,则进入等待,直到出现以下三种情况:-->阻塞

  • 被唤醒
  • 等待时间超时
  • 当前线程被中断

3、三种出对对比:

poll():如果没有元素,直接返回null;如果有元素,出队
take():如果队列空了,一直阻塞,直到数组不为空或者线程被中断-->阻塞
poll(long timeout, TimeUnit unit):如果数组不空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间未超时,则进入等待,直到出现以下三种情况:

  • 被唤醒
  • 等待时间超时
  • 当前线程被中断

注意:在阅读本文之前或在阅读的过程中,需要用到ReentrantLock,内容见《 ReentrantLock源码解析1--获得非公平锁与公平锁lock()》《 ReentrantLock源码解析2--释放锁unlock()》《 ReentrantLock总结

参考

http://www.cnblogs.com/java-zhao/p/5135410.html
源码剖析AQS在几个同步工具类中的使用
ReentrantLock源码解析1--获得非公平锁与公平锁lock()
ReentrantLock源码解析2--释放锁unlock()
ReentrantLock总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 第三章 Java内存模型 3.1 Java内存模型的基础 通信在共享内存的模型里,通过写-读内存中的公共状态进行隐...
    泽毛阅读 4,288评论 2 22
  • 一、多线程 说明下线程的状态 java中的线程一共有 5 种状态。 NEW:这种情况指的是,通过 New 关键字创...
    Java旅行者阅读 4,596评论 0 44
  • 《人民的名义》大火,人们对达康书记越来越喜爱,亲切的称呼“背锅侠”,“耿直boy”,然而他事业成功的背后,是一段分...
    叶小小辞阅读 1,172评论 2 2
  • 儿时的记忆都是虚幻的,从某种角度来说,儿时的记忆是我们对人生和人性美好期望的寄托,也是对无奈现实的妥协。 我出生在...
    苏州奶茶阅读 190评论 2 2
  • 那一年我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。 生命中任何一...
    赵董小姐阅读 3,192评论 42 97