BlockingQueue——从入门到深究

参考及引用声明:

Java多线程进阶(三一)—— J.U.C之collections框架:BlockingQueue接口

不怕难之BlockingQueue及其实现

ReentrantLock(重入锁)功能详解和应用演示

Java之BlockingQueue

BlockingQueue深入解析-BlockingQueue看这一篇就够了

ThreadPoolExecutor线程池解析与BlockingQueue的三种实现

一、BlockingQueue?阻塞队列?

BlockingQueue即阻塞队列,一个指定长度的队列(“先进先出”)**,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止**。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。

通过加“锁”实现线程安全的一个“容器”。

场景类比:还是上厕所的例子。某单位的厕所有3个厕位,一开始,都在“等待”被上,先后进来5个人,先进来的3个人抢占到了位置,并且关门上“锁”,其他两个人就要排队“等待” 。这个时候,厕所就相当于一个长度为3的阻塞队列。

常用于实现生产者与消费者模式,“生产者”和“消费者”是相互独立的,两者之间的通信需要依靠一个队列。这个队列,其实就是所谓的“阻塞队列”。“阻塞队列”的最大好处就是解耦,使“生产者”和“消费者”之间解耦,互不影响的。

扩展:生产者-消费者模式 Producer-Consumer Pattern

生产者和消费者在为不同的处理线程,生产者必须将数据安全地交给消费者,消费者进行消费时,如果生产者还没有建立数据,则消费者需要等待。

类比的例子里,高铁站可以看成是生产者,一直源源不断“产生”要坐的士的乘客。而的士,就可以看成消费者,一直在“消费”那些乘客。

BlockingQueue——从入门到深究_第1张图片

Channel从Producer参与者处接受Data参与者,并保管起来,并应Consumer参与者的要求,将Data参与者传送出去。为确保安全性,Producer参与者与Consumer参与者要对访问共享互斥。

BlockingQueue——从入门到深究_第2张图片

出处:https://segmentfault.com/a/1190000015558655

 

二:ReentrantLock(可重入锁)

BlockingQueue它是基于ReentrantLock。我们需要先了解ReentrantLock(可重入锁)。

ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。

1,与synchronized的对比:

jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。

ReentrantLock(灵活)和synchronized(隐式,自动)都是独占锁,只允许线程互斥的访问临界区

ReentrantLock和(重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样)synchronized(可以放在被递归执行,不用担心释放问题)都是可重入的

2,默认(和synchronized一样)是非公平锁

也可通过传参new ReentrantLock(true)实现公平。

3,ReentrantLock实现简单的阻塞队列

(类包括ReentrantLock,LinkedList,用于唤醒和等待的Condition对象),参见ReentrantLock(重入锁)功能详解和应用演示

  • 可以响应中断的获取锁的方法lockInterruptibly():可以用来解决死锁问题。

三:BlockingQueue源码剖析

BlockingQueue继承了Queue接口,可以看到,对于每种基本方法,“抛出异常”和“返回特殊值”的方法定义和Queue是完全一样的。BlockingQueue只是增加了两类和阻塞相关的方法:put(e)take()offer(e, time, unit)poll(time, unit)

同时,BlockingQueue队列中不能包含null元素。

BlockingQueue——从入门到深究_第3张图片

  • BlockingQueue的核心方法:

public interface BlockingQueue extends Queue {
​
    //将给定元素设置到队列中,如果设置成功返回true, 否则返回false。如果是往限定了长度的队列中设置值,推荐使用offer()方法。
    boolean add(E e);
​
    //将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
    boolean offer(E e);
​
    //将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
    void put(E e) throws InterruptedException;
​
    //将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
​
    //从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
    E take() throws InterruptedException;
​
    //在给定的时间里,从队列中获取值,时间到了直接调用普通的poll方法,为null则直接返回null。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;
​
    //获取队列中剩余的空间。
    int remainingCapacity();
​
    //从队列中移除指定的值。
    boolean remove(Object o);
​
    //判断队列中是否拥有该值。
    public boolean contains(Object o);
​
    //将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection c);
​
    //指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection c, int maxElements);
}

 

四:常用实现类及部分源码解析

BlockingQueue接口的实现类都必须是线程安全的,实现类一般通过“锁”保证线程安全

BlockingQueue——从入门到深究_第4张图片

BlockingQueue——从入门到深究_第5张图片

1,ArrayBlockingQueue 数组阻塞队列

参考:Java多线程进阶(三二)—— J.U.C之collections框架:ArrayBlockingQueue

  • 基于定长数组的阻塞队列,只有一个锁(入队和出队都用同一个锁),按照先进先出(FIFO)的原则对元素进行排序。
  • 可以控制对象的内部锁是否采用公平锁,默认采用非公平锁

BlockingQueue——从入门到深究_第6张图片

  • 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。 一旦创建了这样的缓存区,就不能再增加其容量。(数组大小在构造函数指定,而且从此以后不可改变
  • 试图向已满队列中放入元素会导致放入操作受阻塞,直到BlockingQueue里有新的唤空间才会被醒继续操作;
  • 试图从空队列中检索元素将导致类似阻塞,直到BlocingkQueue进了新货才会被唤醒。

扩展:公平锁 与 非公平锁

  • 公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得

  • 非公平锁:线程加锁时直接尝试获取锁,获取不到就自动到队尾等待。

更多的是直接使用非公平锁:非公平锁比公平锁性能高5-10倍,因为公平锁需要在多核情况下维护一个队列,如果当前线程不是队列的第一个无法获取锁,增加了线程切换次数。

  • 部分源码及分析:
public class ArrayBlockingQueue extends AbstractQueue
    implements BlockingQueue, java.io.Serializable {

    /**
     * 内部数组
     */
    final Object[] items;

    /**
     * 下一个待删除位置的索引: take, poll, peek, remove方法使用
     */
    int takeIndex;

    /**
     * 下一个待插入位置的索引: put, offer, add方法使用
     */
    int putIndex;

    /**
     * 队列中的元素个数
     */
    int count;

    /**
     * 全局锁
     */
    final ReentrantLock lock;

    /**
     * 非空条件队列:当队列空时,线程在该队列等待获取
     */
    private final Condition notEmpty;

    /**
     * 非满条件队列:当队列满时,线程在该队列等待插入
     */
    private final Condition notFull;



/**
 * 指定队列初始容量和公平/非公平策略的构造器.
 */
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();
}



// put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,
//直到队列有空档才会唤醒执行添加操作。但如果队列没有满,
//那么就直接调用enqueue(e)方法将元素加入到数组队列中
​
public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //当队列元素个数与数组长度相等时,无法添加元素,之所以这样做,是防止线程被意外唤醒,不经再次判断就直接调用enqueue方法。
            while (count == items.length)
                //将当前调用线程挂起,添加到notFull条件队列中等待唤醒
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
​
​
//入队操作
//add方法和offer方法最终调用的是enqueue(E x)方法,其方法内部通过putIndex索引直接将元素添加到数组items中,
//这里可能会疑惑的是当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0
    private void enqueue(E x) {
        final Object[] items = this.items;
        //通过putIndex索引对数组进行赋值
        items[putIndex] = x;
        //索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
​
​
public E poll() {
      final ReentrantLock lock = this.lock;
       lock.lock();
       try {
           //判断队列是否为null,不为null执行dequeue()方法,否则返回null
           return (count == 0) ? null : dequeue();
       } finally {
           lock.unlock();
       }
    }
    //删除队列头元素并返回
    private E dequeue() {
     //拿到当前数组的数据
     final Object[] items = this.items;
      @SuppressWarnings("unchecked")
      //获取要删除的对象
      E x = (E) items[takeIndex];
      //将数组中takeIndex索引位置设置为null
      items[takeIndex] = null;
      //takeIndex索引加1并判断是否与数组长度相等,
      //如果相等说明已到尽头,恢复为0
      if (++takeIndex == items.length)
          takeIndex = 0;
      count--;//队列个数减1
      if (itrs != null)
          itrs.elementDequeued();//同时更新迭代器中的元素数据
      //删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
      notFull.signal();
      return x;
    }
​
public boolean remove(Object o) {
        if (o == null) return false;
        //获取数组数据
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            //如果此时队列不为null,这里是为了防止并发情况
            if (count > 0) {
                //获取下一个要添加元素时的索引
                final int putIndex = this.putIndex;
                //获取当前要被删除元素的索引
                int i = takeIndex;
                //执行循环查找要删除的元素
                do {
                    //找到要删除的元素
                    if (o.equals(items[i])) {
                        removeAt(i);//执行删除
                        return true;//删除成功返回true
                    }
                    //当前删除索引执行加1后判断是否与数组长度相等
                    //若为true,说明索引已到数组尽头,将i设置为0
                    if (++i == items.length)
                        i = 0; 
                } while (i != putIndex);//继承查找
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
​
    //根据索引删除元素,实际上是把删除索引之后的元素往前移动一个位置
    void removeAt(final int removeIndex) {
​
     final Object[] items = this.items;
      //先判断要删除的元素是否为当前队列头元素
      if (removeIndex == takeIndex) {
          //如果是直接删除
          items[takeIndex] = null;
          //当前队列头元素加1并判断是否与数组长度相等,若为true设置为0
          if (++takeIndex == items.length)
              takeIndex = 0;
          count--;//队列元素减1
          if (itrs != null)
              itrs.elementDequeued();//更新迭代器中的数据
      } else {
      //如果要删除的元素不在队列头部,
      //那么只需循环迭代把删除元素后面的所有元素往前移动一个位置
          //获取下一个要被添加的元素的索引,作为循环判断结束条件
          final int putIndex = this.putIndex;
          //执行循环
          for (int i = removeIndex;;) {
              //获取要删除节点索引的下一个索引
              int next = i + 1;
              //判断是否已为数组长度,如果是从数组头部(索引为0)开始找
              if (next == items.length)
                  next = 0;
               //如果查找的索引不等于要添加元素的索引,说明元素可以再移动
              if (next != putIndex) {
                  items[i] = items[next];//把后一个元素前移覆盖要删除的元
                  i = next;
              } else {
              //在removeIndex索引之后的元素都往前移动完毕后清空最后一个元素
                  items[i] = null;
                  this.putIndex = i;
                  break;//结束循环
              }
          }
          count--;//队列元素减1
          if (itrs != null)
              itrs.removedAt(removeIndex);//更新迭代器数据
      }
      notFull.signal();//唤醒添加线程
    }
​
​
//从队列头部删除,队列没有元素就阻塞,可中断
     public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
          lock.lockInterruptibly();//中断
          try {
              //如果队列没有元素
              while (count == 0)
                  //执行阻塞操作
                  notEmpty.await();
              return dequeue();//如果队列有元素执行删除操作
          } finally {
              lock.unlock();
          }
        }

......

}

 

从上面的入队/出队操作,可以看出,ArrayBlockingQueue的内部数组其实是一种环形结构。

假设ArrayBlockingQueue的容量大小为6,我们来看下整个入队过程:

①初始时

BlockingQueue——从入门到深究_第7张图片

②插入元素“9”

BlockingQueue——从入门到深究_第8张图片

③插入元素“2”、“10”、“25”、“93”

BlockingQueue——从入门到深究_第9张图片

④插入元素“90”

注意,此时再插入一个元素“90”,则putIndex变成6,等于队列容量6,由于是循环队列,所以会将tableIndex重置为0:

BlockingQueue——从入门到深究_第10张图片

这是队列已经满了(count==6),如果再有线程尝试插入元素,并不会覆盖原有值,而是被阻塞。


我们再来看下出队过程:

①出队元素“9”

BlockingQueue——从入门到深究_第11张图片

②出队元素“2”、“10”、“25”、“93”

BlockingQueue——从入门到深究_第12张图片

③出队元素“90”

注意,此时再出队一个元素“90”,则tabeIndex变成6,等于队列容量6,由于是循环队列,所以会将tableIndex重置为0:

BlockingQueue——从入门到深究_第13张图片

这是队列已经空了(count==0),如果再有线程尝试出队元素,则会被阻塞。

总结:ArrayBlockingQueue利用了ReentrantLock来保证线程的安全性,针对队列的修改都需要加全局锁。在一般的应用场景下已经足够。对于超高并发的环境,由于生产者-消息者共用一把锁,可能出现性能瓶颈。

2,LinkedBlockingQueue 链表阻塞队列

参考:Java多线程进阶(三三)—— J.U.C之collections框架:LinkedBlockingQueue

  • 一个由链表结构组成的双向阻塞队列

  • 入队和出队采用来个独立的锁来控制数据同步(生产者和消费者可以并行,能高效的处理并发数据),多线程并发时,可以将锁的竞争最多降到一半。

  • 构造时最好指定容量大小,没有则默认一个类似无限大小的容量(Integer.MAX_VALUE)————存在风险:如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽

  • 内部是使用链表实现一个队列的,但是却有别于一般的队列,在于该队列至少有一个节点,头节点不含有元素。

扩展:LinkedBlockingQueueArrayBlockingQueue比较主要有以下区别:

\1. 队列大小不同。ArrayBlockingQueue初始构造时必须指定大小,而LinkedBlockingQueue构造时既可以指定大小,也可以不指定(默认为Integer.MAX_VALUE,近似于无界);

\2. 底层数据结构不同。ArrayBlockingQueue底层采用数组作为数据存储容器,而LinkedBlockingQueue底层采用单链表作为数据存储容器;

\3. 两者的加锁机制不同。ArrayBlockingQueue使用一把全局锁,即入队和出队使用同一个ReentrantLock锁;而LinkedBlockingQueue进行了锁分离,入队使用一个ReentrantLock锁(putLock),出队使用另一个ReentrantLock锁(takeLock);

\4. LinkedBlockingQueue不能指定公平/非公平策略(默认都是非公平),而ArrayBlockingQueue可以指定策略。

\5. ArrayBlockingQueue在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。

部分源码:

/**
 * 默认构造器.
 * 队列容量为Integer.MAX_VALUE.
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
​
/**
 * 显示指定队列容量的构造器
 */
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node(null);
}
​
​
​
/**
 * 从已有集合构造队列.
 * 队列容量为Integer.MAX_VALUE
 */
public LinkedBlockingQueue(Collection c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock();                     // 这里加锁仅仅是为了保证可见性
    try {
        int n = 0;
        for (E e : c) {
            if (e == null)              // 队列不能包含null元素
                throw new NullPointerException();
            if (n == capacity)          // 队列已满
                throw new IllegalStateException("Queue full");
            enqueue(new Node(e));    // 队尾插入元素
            ++n;
        }
        count.set(n);                   // 设置元素个数
    } finally {
        putLock.unlock();
    }
}
​
​
//节点类,用于存储数据
    static class Node {
        E item;
        Node next;
​
        Node(E x) { item = x; }
    }
    // 容量大小
    private final int capacity;
​
    // 使用了一个原子变量AtomicInteger记录队列中元素的个数,以保证入队/出队并发修改元素时的数据一致性。,因为有2个锁,存在竞态条件,使用AtomicInteger
    private final AtomicInteger count = new AtomicInteger();
​
    // 头结点
    private transient Node head;
​
    // 尾节点
    private transient Node last;
​
    // 获取并移除元素时使用的锁,如take, poll, etc
    private final ReentrantLock takeLock = new ReentrantLock();
​
    // notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程
    private final Condition notEmpty = takeLock.newCondition();
​
    // 添加元素时使用的锁如 put, offer, etc 
    private final ReentrantLock putLock = new ReentrantLock();
​
    // notFull条件对象,当队列数据已满时用于挂起执行添加的线程 
    private final Condition notFull = putLock.newCondition();
​
​
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
​
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node(null);
    }
​
    public LinkedBlockingQueue(Collection c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }
​
​
/**
 * 在队尾插入指定的元素.
 * 如果队列已满,则阻塞线程.
 */
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    int c = -1;
    Node node = new Node(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();            // 获取“入队锁”
    try {
        while (count.get() == capacity) {   // 队列已满, 则线程在notFull上等待
            notFull.await();
        }
        enqueue(node);                      // 将新结点链接到“队尾”
​
        /**
         * c+1 表示的元素个数.
         * 如果,则唤醒一个“入队线程”
         */
        c = count.getAndIncrement();        // c表示入队前的队列元素个数
        if (c + 1 < capacity)               // 入队后队列未满, 则唤醒一个“入队线程”
            notFull.signal();
    } finally {
        putLock.unlock(); // 释放锁,下面可能会获得出队的锁,避免死锁
    }
//每入队一个元素,都要判断下队列是否空了,如果空了,说明可能存在正在等待的“出队线程”,需要唤醒它
    if (c == 0)                             // 队列初始为空, 则唤醒一个“出队线程”
        signalNotEmpty();
}
​
​
/**
 * 从队首出队一个元素
 */
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;   // 获取“出队锁”
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {                  // 队列为空, 则阻塞线程
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();                // c表示出队前的元素个数
        if (c > 1)                                  // 出队前队列非空, 则唤醒一个出队线程
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
// 每入队一个元素,都要判断下队列是否满,如果是满的,说明可能存在正在等待的“入队线程”,需要唤醒它
    if (c == capacity)                              // 队列初始为满,则唤醒一个入队线程
        signalNotFull();
    return x;
}
​
/**
 * 队首出队一个元素.
 */
private E dequeue() {
    Node h = head;
    Node first = h.next;
    h.next = h;         // 原来的head指向自己,help GC
    head = first;
    E x = first.item;
    first.item = null; // 头结点置空
    return x;
}

 

3,PriorityBlockingQueue 基于优先级的阻塞队列

  • 优先级的判断通过构造函数传入的Compator对象来决定

  • 不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者——生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间

  • 内部控制线程同步的锁采用的是公平锁

  • public class PriorityBlockingQueue extends AbstractQueue
            implements BlockingQueue, java.io.Serializable {
     
        /**
         * 默认容量.
         */
        private static final int DEFAULT_INITIAL_CAPACITY = 11;
     
        /**
         * 最大容量.
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
     
        /**
         * 内部堆数组, 保存实际数据, 可以看成一颗二叉树:
         * 对于顶点queue[n], queue[2*n+1]表示左子结点, queue[2*(n+1)]表示右子结点.
         */
        private transient Object[] queue;
     
        /**
         * 队列中的元素个数.
         */
        private transient int size;
     
        /**
         * 比较器, 如果为null, 表示以元素自身的自然顺序进行比较(元素必须实现Comparable接口).
         */
        private transient Comparator comparator;
     
        /**
         * 全局锁.
         */
        private final ReentrantLock lock;
     
        /**
         * 当队列为空时,出队线程在该条件队列上等待.
         */
        private final Condition notEmpty;
     
        public class PriorityBlockingQueue extends AbstractQueue
            implements BlockingQueue, java.io.Serializable {
     
        /**
         * 默认容量.
         */
        private static final int DEFAULT_INITIAL_CAPACITY = 11;
     
        /**
         * 最大容量.
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
     
        /**
         * 内部堆数组, 保存实际数据, 可以看成一颗二叉树:
         * 对于顶点queue[n], queue[2*n+1]表示左子结点, queue[2*(n+1)]表示右子结点.
         */
        private transient Object[] queue;
     
        /**
         * 队列中的元素个数.
         */
        private transient int size;
     
        /**
         * 比较器, 如果为null, 表示以元素自身的自然顺序进行比较(元素必须实现Comparable接口).
         */
        private transient Comparator comparator;
     
        /**
         * 全局锁.
         */
        private final ReentrantLock lock;
     
        /**
         * 当队列为空时,出队线程在该条件队列上等待.
         */
        private final Condition notEmpty;
     
        /**
     * 将元素x插入到array[k]的位置.
     * 然后按照元素的自然顺序进行堆调整——"上浮",以维持"堆"有序.
     * 最终的结果是一个"小顶堆".
     */
    private static  void siftUpComparable(int k, T x, Object[] array) {
        Comparable key = (Comparable) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;     // 相当于(k-1)除2, 就是求k结点的父结点索引parent
            Object e = array[parent];
            if (key.compareTo((T) e) >= 0)  // 如果插入的结点值大于父结点, 则退出
                break;
     
            // 否则,交换父结点和当前结点的值
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }
    }
    
    
    
    private void tryGrow(Object[] array, int oldCap) {
        lock.unlock();  // 扩容和入队/出队可以同时进行, 所以先释放全局锁
        Object[] newArray = null;
        if (allocationSpinLock == 0 &&
                UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                        0, 1)) {    // allocationSpinLock置1表示正在扩容
            try {
                // 计算新的数组大小
                int newCap = oldCap + ((oldCap < 64) ?
                        (oldCap + 2) :
                        (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // 溢出判断
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];  // 分配新数组
            } finally {
                allocationSpinLock = 0;
            }
        }
        if (newArray == null)   // 扩容失败(可能有其它线程正在扩容,导致allocationSpinLock竞争失败)
            Thread.yield();
        
        lock.lock();            // 获取全局锁(因为要修改内部数组queue)
        if (newArray != null && queue == array) {
            queue = newArray;   // 指向新的内部数组
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }
    
    
    /**
     * 出队一个元素.
     * 如果队列为空, 则阻塞线程.
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();   // 获取全局锁
        E result;
        try {
            while ((result = dequeue()) == null)    // 队列为空
                notEmpty.await();                   // 线程在noEmpty条件队列等待
        } finally {
            lock.unlock();
        }
        return result;
    }
     
    private E dequeue() {
        int n = size - 1;   // n表示出队后的剩余元素个数
        if (n < 0)          // 队列为空, 则返回null
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0];    // array[0]是堆顶结点, 每次出队都删除堆顶结点
            E x = (E) array[n];         // array[n]是堆的最后一个结点, 也就是二叉树的最右下结点
            array[n] = null;
            Comparator cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }
    
    
    /**
     * 堆的"下沉"调整.
     * 删除array[k]对应的结点,并重新调整堆使其有序.
     *
     * @param k     待删除的位置
     * @param x     待比较的健
     * @param array 堆数组
     * @param n     堆的大小
     */
    private static  void siftDownComparable(int k, T x, Object[] array, int n) {
        if (n > 0) {
            Comparable key = (Comparable) x;
            int half = n >>> 1;           // 相当于n除2, 即找到索引n对应结点的父结点
            while (k < half) {
                /**
                 * 下述代码中:
                 * c保存k的左右子结点中的较小结点值 
                 * child保存较小结点对应的索引
                 */
                int child = (k << 1) + 1; // k的左子结点
                Object c = array[child];
     
                int right = child + 1;    // k的右子结点
                if (right < n && ((Comparable) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }
    
    
    
    }

    堆的“上浮”调整

    我们通过示例来理解下入队的整个过程:假设初始构造的队列大小为6,依次插入9、2、93、10、25、90

    ①初始队列情况

    clipboard.png


    ②插入元素9(索引0处)

    clipboard.png

    将上述数组想象成一棵完全二叉树,其实就是下面的结构:
    clipboard.png


    ③插入元素2(索引1处)

    clipboard.png

    对应的二叉树:
    BlockingQueue——从入门到深究_第14张图片

    由于结点2的父结点为9,所以要进行“上浮调整”,最终队列结构如下:
    clipboard.png

    BlockingQueue——从入门到深究_第15张图片


    ④插入元素93(索引2处)

    clipboard.png

    BlockingQueue——从入门到深究_第16张图片


    ⑤插入元素10(索引3处)

    clipboard.png

    BlockingQueue——从入门到深究_第17张图片


    ⑥插入元素25(索引4处)

    clipboard.png

    BlockingQueue——从入门到深究_第18张图片


    ⑦插入元素90(索引5处)

    clipboard.png

    BlockingQueue——从入门到深究_第19张图片

    此时,堆不满足有序条件,因为“90”的父结点“93”大于它,所以需要“上浮调整”:

    clipboard.png

    BlockingQueue——从入门到深究_第20张图片

    最终,堆的结构如上,可以看到,经过调整后,堆顶元素一定是最小的。

    堆的“下沉”调整

    来看个示例,假设堆的初始结构如下,现在出队一个元素(索引0位置的元素2)。

    ①初始状态

    clipboard.png

    对应二叉树结构:

    BlockingQueue——从入门到深究_第21张图片


    ②将顶点与最后一个结点调换

    即将顶点“2”与最后一个结点“93”交换,然后将索引5为止置null。

    BlockingQueue——从入门到深究_第22张图片

    注意: 为了提升效率(比如siftDownComparable的源码所示)并不一定要真正交换,可以用一个变量保存索引5处的结点值,在整个下沉操作完成后再替换。但是为了理解这一过程,示例图中全是以交换进行的。

    ③下沉索引0处结点

    比较元素“93”和左右子结点中的最小者,发现“93”大于“9”,违反了“小顶堆”的规则,所以交换“93”和“9”,这一过程称为siftdown(下沉)

    BlockingQueue——从入门到深究_第23张图片


    ④继续下沉索引1处结点

    比较元素“93”和左右子结点中的最小者,发现“93”大于“10”,违反了“小顶堆”的规则,所以交换“93”和“10”:

    BlockingQueue——从入门到深究_第24张图片


    ⑤比较结束

    由于“93”已经没有左右子结点了,所以下沉结束,可以看到,此时堆恢复了有序状态,最终队列结构如下:

    clipboard.png

4,DelayQueue 延时阻塞队列

  • 一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有当其指定的延迟时间到了,才能够从队列中获取到该元素

  • 没有大小限制的队列

  • 入队(生产者)不阻塞,出队(消费者)阻塞

  • DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。

扩展:

Leader/Followers模式:

  1. 有若干个线程(一般组成线程池)用来处理大量的事件

  2. 有一个线程作为领导者,等待事件的发生;其他的线程作为追随者,仅仅是睡眠。

  3. 假如有事件需要处理,领导者会从追随者中指定一个新的领导者,自己去处理事件。

  4. 唤醒的追随者作为新的领导者等待事件的发生。

  5. 处理事件的线程处理完毕以后,就会成为追随者的一员,直到被唤醒成为领导者。

  6. 假如需要处理的事件太多,而线程数量不够(能够动态创建线程处理另当别论),则有的事件可能会得不到处理。

所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。

源码分析可参考:https://www.cnblogs.com/WangHaiMing/p/8798709.html

  • 应用场景:

(1)使用一个DelayQueue来管理一个超时未响应的连接队列。

(2)缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

(3)定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

 

5,SynchronousQueue 同步队列

  • 一种无缓冲的等待队列,类似于无中介的直接交易。一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。

  • 声明一个SynchronousQueue有公平模式和非公平模式两种不同的方式,它们之间有着不太一样的行为。

公平模式和非公平模式的区别:   

SynchronousQueue根据公平/非公平访问策略的不同,内部使用了两种不同的数据结构:栈和队列。对于公平策略,内部构造了一个TransferQueue对象,而非公平策略则是构造了TransferStack对象。这两个类都继承了内部类Transferer,SynchronousQueue中的所有方法,其实都是委托调用了TransferQueue/TransferStack的方法

非公平模式:非公平策略由TransferStack类实现,既然TransferStack是栈,那就有结点。TransferStack内部定义了名为SNode的结点:

static final class SNode {
    volatile SNode next;
    volatile SNode match;       // 与当前结点配对的结点
    volatile Thread waiter;     // 当前结点对应的线程
    Object item;                // 实际数据或null
    int mode;                   // 结点类型
 
    SNode(Object item) {
        this.item = item;
    }
  
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long matchOffset;
    private static final long nextOffset;
 
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = SNode.class;
            matchOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("match"));
            nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    // ...

}


static final class TransferStack extends Transferer {
 
    /**
     * 未配对的消费者
     */
    static final int REQUEST = 0;
    /**
     * 未配对的生产者
     */
    static final int DATA = 1;
    /**
     * 配对成功的消费者/生产者
     */
    static final int FULFILLING = 2;
 
     volatile SNode head;
 
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long headOffset;
 
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = TransferStack.class;
            headOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("head"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
 
    // ...
}

上述SNode结点的定义中有个mode字段,表示结点的类型。TransferStack一共定义了三种结点类型,任何线程对TransferStack的操作都会创建下述三种类型的某种结点:

  • REQUEST:表示未配对的消费者(当线程进行出队操作时,会创建一个mode值为REQUEST的SNode结点 )
  • DATA:表示未配对的生产者(当线程进行入队操作时,会创建一个mode值为DATA的SNode结点 )
  • FULFILLING:表示配对成功的消费者/生产者

核心操作——put/take

/**
 * 入队指定元素e.
 * 如果没有另一个线程进行出队操作, 则阻塞该入队线程.
 */
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}

/**
 * 出队一个元素.
 * 如果没有另一个线程进行入队操作, 则阻塞该出队线程.
 */
public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

/**
 * 入队/出队一个元素.
 * SynchronousQueue一样不支持null元素,实际的入队/出队操作都是委托给了transfer方法,该方法返回null表示出/入队失败(通常是线程被中断或超时)
 */
E transfer(E e, boolean timed, long nanos) {
    SNode s = null; // s表示新创建的结点
    // 入参e==null, 说明当前是出队线程(消费者), 否则是入队线程(生产者)
    // 入队线程创建一个DATA结点, 出队线程创建一个REQUEST结点
    int mode = (e == null) ? REQUEST : DATA;

    for (; ; ) {    // 自旋
        SNode h = head;
        if (h == null || h.mode == mode) {          // CASE1: 栈为空 或 栈顶结点类型与当前mode相同
            if (timed && nanos <= 0) {              // case1.1: 限时等待的情况
                if (h != null && h.isCancelled())
                    casHead(h, h.next);
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {  // case1.2 将当前结点压入栈
                SNode m = awaitFulfill(s, timed, nanos);        // 阻塞当前调用线程
                if (m == s) {                                   // 阻塞过程中被中断
                    clean(s);
                    return null;
                }

                // 此时m为配对结点
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);

                // 入队线程null, 出队线程返回配对结点的值
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
            // 执行到此处说明入栈失败(多个线程同时入栈导致CAS操作head失败),则进入下一次自旋继续执行

        } else if (!isFulfilling(h.mode)) {          // CASE2: 栈顶结点还未配对成功
            if (h.isCancelled())                     // case2.1: 元素取消情况(因中断或超时)的处理
                casHead(h, h.next);
            else if (casHead(h, s = snode(s, e,
                h, FULFILLING | mode))) {      // case2.2: 将当前结点压入栈中
                for (; ; ) {
                    SNode m = s.next;       // s.next指向原栈顶结点(也就是与当前结点匹配的结点)
                    if (m == null) {        // m==null说明被其它线程抢先匹配了, 则跳出循环, 重新下一次自旋
                        casHead(s, null);
                        s = null;
                        break;
                    }

                    SNode mn = m.next;
                    if (m.tryMatch(s)) {    // 进行结点匹配
                        casHead(s, mn);     // 匹配成功, 将匹配的两个结点全部弹出栈
                        return (E) ((mode == REQUEST) ? m.item : s.item);   // 返回匹配值
                    } else                  // 匹配失败
                        s.casNext(m, mn);   // 移除原待匹配结点
                }
            }
        } else {                            // CASE3: 其它线程正在匹配
            SNode m = h.next;
            if (m == null)                  // 栈顶的next==null, 则直接弹出, 重新进入下一次自旋
                casHead(h, null);
            else {                          // 尝试和其它线程竞争匹配
                SNode mn = m.next;
                if (m.tryMatch(h))
                    casHead(h, mn);         // 匹配成功
                else
                    h.casNext(m, mn);       // 匹配失败(被其它线程抢先匹配成功了)
            }
        }
    }
}

整个transfer方法考虑了限时等待的情况,且入队/出队其实都是调用了同一个方法,其主干逻辑就是在一个自旋中完成以下三种情况之一的操作,直到成功,或者被中断或超时取消:

  1. 栈为空,或栈顶结点类型与当前入队结点相同。这种情况,调用线程会阻塞;
  2. 栈顶结点还未配对成功,且与当前入队结点可以配对。这种情况,直接进行配对操作;
  3. 栈顶结点正在配对中。这种情况,直接进行下一个结点的配对。
  • 应用场景:

SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

 

五:阻塞队列的应用实例

(以下内容大部分参考ThreadPoolExecutor线程池解析与BlockingQueue的三种实现)

据了解,在 线程池,future,futuretask,runnable,callable等地方都运用到了阻塞队列。

扩展:在Java5之前,线程是没有返回值的,可返回值的任务必须实现Callable接口,类似的,无返回值的任务必须Runnable接口。执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。

下面以 线程池 为例做针对性分析:

线程池:

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                         BlockingQueue workQueue,
                         ThreadFactory threadFactory,
                         RejectedExecutionHandler handler){...}

TimeUnit:时间单位;BlockingQueue:等待的线程存放队列;keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收;RejectedExecutionHandler:线程池对拒绝任务的处理策略。 自定义线程池:这个构造方法对于队列是什么类型比较关键。队列在线程池中是非常重要的角色,那么Executors就是根据不同的队列实现了功能不同的线程池。

线程池工作方式:

  • 在使用有界队列时,若有新的任务需要执行,如果线程池实际线程数小于corePoolSize,则优先创建线程,

  • 若大于corePoolSize,则会将任务加入队列,

  • 若队列已满,则在总线程数不大于maximumPoolSize的前提下,创建新的线程,

  • 若队列已经满了且线程数大于maximumPoolSize,则执行拒绝策略。或其他自定义方式。

来源:Executors包含的常用线程池

1.ExecutorService newFixedThreadPool(int nThreads):固定大小线程池

public static ExecutorService newFixedThreadPool(int nThreads) {  
        return new ThreadPoolExecutor(nThreads, nThreads,  
                                      0L, TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue());  
    }  

 

coresize和maxsize相同,超时时间为0,队列用的LinkedBlockingQueue无界的FIFO队列,这表示什么,很明显,这个线程池始终只有

2.ExecutorService newCachedThreadPool():无界线程池

public static ExecutorService newCachedThreadPool() {  
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
                                      60L, TimeUnit.SECONDS,  
                                      new SynchronousQueue());  
    }

 

SynchronousQueue队列,一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。所以,当我们提交第一个任务的时候,是加入不了队列的,这就满足了,一个线程池条件“当无法加入队列的时候,且任务没有达到maxsize时,我们将新开启一个线程任务”。所以我们的maxsize是big big。时间是60s,当一个线程没有任务执行会暂时保存60s超时时间,如果没有的新的任务的话,会从cache中remove掉

3.Executors.newSingleThreadExecutor();大小为1的固定线程池,这个其实就是newFixedThreadPool(1).关注newFixedThreadPool的用法就行

 

(因个人能力,时间精力等原因,仍有不足,后续将继续完善。。。)


结束和声明

以上纯属个人观点和体会,相关的资料和观点来自网络的朋友们! 希望这篇文章能对你有所帮助! 欢迎大家来一起讨论分享干货,或者批评指正! 更加热切盼望各路大神前辈给些指导和建议! 转载请注明出处!或者联系我!([email protected]

你可能感兴趣的:(学习干货,多线程,难重点)