阻塞队里总结与重要源码解析

1.java中的阻塞队列

阻塞队里总结与重要源码解析_第1张图片

BlockingQueue为阻塞队列的顶级接口,下边有几种阻塞队列:

ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。

2.Queue接口中对应的方法说明

操作 抛出异常 返回特殊值
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()
  • add方法在将一个元素插入到队列的尾部时,如果出现队列已经满了,那么就会抛出IllegalStateException,而使用offer方法时,如果队列满了,则添加失败,返回false,但并不会引发异常。
  • remove方法是获取队列的头部元素并且删除,如果当队列为空时,那么就会抛出NoSuchElementException。而poll在队列为空时,则返回一个null。
  • element方法是从队列中获取到队列的第一个元素,但不会删除,但是如果队列为空时,那么它就会抛出NoSuchElementException。peek方法与之类似,只是不会抛出异常,而是返回null。

2.BlockingQueue接口中对应的方法说明

操作 抛出异常 返回特殊值 阻塞 超时
Insert add(e) offer(e) put(e) offer(e, time, unit)
Remove remove() poll() take() poll(time, unit)
Examine element() peek()

BlockingQueue虽然比起Queue在操作上提供了更多的支持,但是它在使用的使用也应该如下的几点:

  • BlockingQueue中是不允许添加null的,该接受在声明的时候就要求所有的实现类在接收到一个null的时候,都应该抛出NullPointerException。

  • BlockingQueue是线程安全的,因此它的所有和队列相关的方法都具有原子性。但是对于那么从Collection接口中继承而来的批量操作方法,比如addAll(Collection e)等方法,BlockingQueue的实现通常没有保证其具有原子性,因此我们在使用的BlockingQueue,应该尽可能地不去使用这些方法。

  • BlockingQueue主要应用于生产者与消费者的模型中,其元素的添加和获取都是极具规律性的。但是对于remove(Object o)这样的方法,虽然BlockingQueue可以保证元素正确的删除,但是这样的操作会非常响应性能,因此我们在没有特殊的情况下,也应该避免使用这类方法。

3.ArrayBlockingQueue详解

ArrayBlockingQueue 是一个线程安全的、基于数组、有界的、阻塞的、FIFO 队列。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。ArrayBlockingQueue读写共享一把锁

(1)数据结构
/** 数据存放在Object数组中 **/
final Object[] items;

/** 下次拿数据索引的位置:items index for next take, poll, peek or remove */
int takeIndex;

/** 下次放数据索引的位置:items index for next put, offer, or add */
int putIndex;

/** 已有元素的数量:Number of elements in the queue */
int count;

/** 可重入锁:Main lock guarding all access */
final ReentrantLock lock;

/** take的队列:Condition for waiting takes */
private final Condition notEmpty;

/** put的队列:Condition for waiting puts */
private final Condition notFull;
(2)ArrayBlockingQueue初始化
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();
}

ArrayBlockingQueue在实例化的时候创建了两个条件队列notEmpty和notFull两个条件队列,这两个队列公用一把ReentrantLock锁

两个队列均为单向队列,由firstNode和lastNode指针指向首尾,节点为node节点(AQS ReentrantLock知识此处不做赘述)

(3)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();
    }
}
private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
  	//直接给数组赋值,putIndex为当前值的数组索引
    items[putIndex] = x;
  	//如果数组已经满了
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
  	//条件唤醒等待的take线程
    notEmpty.signal();
}
(4)take方法
public E take() throws InterruptedException {
  	//先获取锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
      	//如果数组中的数量为0,则阻塞等待
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }	
}

ArrayBlockQueue.dequeue
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;
	  //如果下次拿数据的索引位置等于数组的长度,则设置为0即表示数组中数据已经拿完
    if (++takeIndex == items.length)
        takeIndex = 0;
  	//数量减一
    count--;
    if (itrs != null)
        itrs.elementDequeued();
  	//条件唤醒等待的put线程
    notFull.signal();
    return x;
}
ArrayBlockQueue.itrs.elementDequeued
void elementDequeued() {
    // assert lock.getHoldCount() == 1;
  //如果队列为空
    if (count == 0)
        queueIsEmpty();
    else if (takeIndex == 0)
        takeIndexWrapped();
}
ArrayBlockQueue.itrs.queueIsEmpty
void queueIsEmpty() {
    // assert lock.getHoldCount() == 1;
  //遍历链表
    for (Node p = head; p != null; p = p.next) {
        Itr it = p.get();
        if (it != null) {
          //将引用清除
            p.clear();
          //通知Itr队列空了,将各参数置为null或者特殊index值
            it.shutdown();
        }
    }
    head = null;
    itrs = null;
}
ArrayBlockQueue.itrs.takeIndexWrapped
void takeIndexWrapped() {
    // assert lock.getHoldCount() == 1;
    cycles++;
  //遍历链表,o表示上一个有效节点,p表示当前遍历的节点
    for (Node o = null, p = head; p != null;) {
        final Itr it = p.get();
        final Node next = p.next;
        if (it == null || it.takeIndexWrapped()) {
            // unlink p
            // assert it == null || it.isDetached();
            p.clear();
            p.next = null;
          //之前的节点是无效节点,重置head,把next之前的节点都移除了
            if (o == null)
                head = next;
            else
              //移除p这一个节点
                o.next = next;
        } else {
          //保存上一个有效节点
            o = p;
        }
       //处理下一个节点
        p = next;
    }
  //没有有效节点,itrs置为null
    if (head == null)   // no more iterators to track
        itrs = null;
}
ArrayBlockQueue.itr.takeIndexWrapped
boolean takeIndexWrapped() {
    // assert lock.getHoldCount() == 1;
    if (isDetached())
        return true;
    if (itrs.cycles - prevCycles > 1) {
        // All the elements that existed at the time of the last
        // operation are gone, so abandon further iteration.
      //上一次操作时存在的所有元素都消失了,因此放弃进一步的迭代
        shutdown();
        return true;
    }
    return false;
}

4.LinkedBlockingQueue详解

(1)LinkedBlockingQueue概述
  • LinkedBlockingQueue是一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存,则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小
  • LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行
(2)LinkedBlockingQueue的类图

阻塞队里总结与重要源码解析_第2张图片

(3)LinkedBlockingQueue属性值
// 容量,指定容量就是有界队列
private final int capacity;
// 元素数量,用原子操作类的原因在于有两个线程都会操作需要保证可见性
private final AtomicInteger count = new AtomicInteger();
// 链表头  本身是不存储任何元素的,初始化时item指向null
transient Node<E> head;
// 链表尾
private transient Node<E> last;
// take锁   锁分离,提高效率
private final ReentrantLock takeLock = new ReentrantLock();
// notEmpty条件
// 当队列无元素时,take锁会阻塞在notEmpty条件上,等待其它线程唤醒
private final Condition notEmpty = takeLock.newCondition();
// put锁
private final ReentrantLock putLock = new ReentrantLock();
// notFull条件
// 当队列满了时,put锁会会阻塞在notFull上,等待其它线程唤醒
private final Condition notFull = putLock.newCondition();

//典型的单链表结构
static class Node<E> {
    E item;  //存储元素
    Node<E> next;  //后继节点    单链表结构
    Node(E x) { item = x; }
}
(4)LinkedBlockingQueue构造函数
//初始化LinkedBlockingQueue,设置默认的容量
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

//初始化LinkedBlockingQueue,设置指定的容量
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

// 初始化LinkedBlockingQueue,将传入的集合元素加入到队列中
public LinkedBlockingQueue(Collection<? extends E> 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) {
          // 如果元素为null,则抛出NPE
            if (e == null)
                throw new NullPointerException();
          // 如果元素数量已经达到设置的容量,则抛出队列已满的异常
            if (n == capacity)
                throw new IllegalStateException("Queue full");
          // 创建节点,并入队
            enqueue(new Node<E>(e));
          // 元素数量加1
            ++n;
        }
      // 设置元素数量
        count.set(n);
    } finally {
        putLock.unlock();
    }
}
(5)put方法源码分析
public void put(E e) throws InterruptedException {
  // 不允许null元素
    if (e == null) throw new NullPointerException()
    int c = -1;
  // 新建一个节点
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
  // 使用put锁加锁
    putLock.lockInterruptibly();
    try {
      // 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
        while (count.get() == capacity) {
            notFull.await();
        }
      // 队列不满,就入队
        enqueue(node);
      // 队列长度加1,返回原值
        c = count.getAndIncrement();
      // 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可以继续入队) 
      // 这里为啥要唤醒一下呢?因为存在情况是,没人获取时,队列满了而且还不断有人塞数据,此时会一大批线程被阻塞,现在有空余位置了,应该被唤醒
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
      //解锁,真正唤醒生产者线程
        putLock.unlock();
    }
  // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
    if (c == 0)
        signalNotEmpty();
}
LinkedBlockingQueue.enqueue
private void enqueue(Node<E> node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
  // 直接加到last后面,last指向入队元素
    last = last.next = node;
}
LinkedBlockingQueue.signalNotEmpty
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
  // 加take锁
    takeLock.lock();
    try {
      // notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
        notEmpty.signal();
    } finally {
      // 解锁,真正唤醒消费者线程
        takeLock.unlock();
    }
}
(6)take方法源码分析
public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
  //加锁
    takeLock.lockInterruptibly();
    try {
      // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
        while (count.get() == 0) {
            notEmpty.await();
        }
      // 否则,出队
        x = dequeue();
        c = count.getAndDecrement();
      // 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
        if (c > 1)
            notEmpty.signal();
    } finally {
      //解锁,真正唤醒消费者线程
        takeLock.unlock();
    }
    // 为什么队列是满的才唤醒阻塞在notFull上的线程呢?
    // 因为唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程,
    // 这也是锁分离带来的代价
    // 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
    if (c == capacity)
        signalNotFull();
    return x;
}
LinkedBlockingQueue.dequeue
private E dequeue() {
    // assert takeLock.isHeldByCurrentThread();
    // assert head.item == null;
  // head节点本身是不存储任何元素的
  // 这里把head删除,并把head下一个节点作为新的值
  // 并把其值置空,返回原来的值
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;
    return x;
}
LinkedBlockingQueue.signalNotFull
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
  //加锁
    putLock.lock();
    try {
      //notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
        notFull.signal();
    } finally {
      //解锁,这才会真正的唤醒生产者线程
        putLock.unlock();
    }
}
(7)LinkedBlockingQueue总结

​ 【1】无界阻塞队列,可以指定容量,默认为 Integer.MAX_VALUE,先进先出,存取互不干扰

【2】数据结构:链表(可以指定容量,默认为 Integer.MAX_VALUE,内部类Node存储元素)

【3】锁分离:存取互不干扰,存取操作的是不同的Node对象(takeLock【取Node节点保证前驱后继不乱】,putLock【存Node节点保证前驱后继不乱】,删除时则两个锁一起加)【这是最大的亮点】

【4】阻塞对象(notEmpty【出队:队列count=0,无元素可取时,阻塞在该对象上】,notFull【入队:队列count=capacity,放不进元素时,阻塞在该对象上】)

【5】入队,从队尾入队,由last指针记录。

【6】出队,从队首出队,由head指针记录。

【7】线程池中采用LinkedBlockingQueue而不采用ArrayBlockingQueue的原因便是因为锁分离带来了性能的提升,大大提高队列的吞吐量。

(8)LinkedBlockingQueue实际使用场景

单生产者,单消费者 用 LinkedBlockingqueue
多生产者,单消费者 用 LinkedBlockingqueue

单生产者 ,多消费者 用 ConcurrentLinkedQueue

多生产者 ,多消费者 用 ConcurrentLinkedQueue

5.ConcurrentLinkedQueue详解

(1)ConcurrentLinkedQueue概述

ConcurrentLinkedQueue是一个无锁化、非阻塞、线程安全的单向队列,JDK1.5提供。ConcurrentLinkedQueue实现的依赖cas操作和java的valotile语义。

(2)ConcurrentLinkedQueue类图结构

阻塞队里总结与重要源码解析_第3张图片

(3)ConcurrentLinkedQueue属性与构造器
public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {
     
    private transient volatile Node<E> head;
    private transient volatile Node<E> tail;

  //无参构造器
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }
  
  //有参构造器,初始化传入集合
    public ConcurrentLinkedQueue(Collection<? extends E> c) {
          Node<E> h = null, t = null;
          for (E e : c) {
              checkNotNull(e);
              Node<E> newNode = new Node<E>(e);
              if (h == null)
                  h = t = newNode;
              else {
                  t.lazySetNext(newNode);
                  t = newNode;
              }
          }
          if (h == null)
              h = t = new Node<E>(null);
          head = h;
          tail = t;
      }
 }
(4)offer方法
public boolean offer(E e) {
	//判断是否为空
    checkNotNull(e);
  //给当前值新建一个节点
    final Node<E> newNode = new Node<E>(e);
	// 2.开始循环,获取尾结点
    for (Node<E> t = tail, p = t;;) {
      	// t和p默认是是尾结点
        // q是p的下一个结点
        Node<E> q = p.next;
      //如果next为空,说明p是尾结点
        if (q == null) {
          // 通过cas的方式放入新结点,未成功则继续循环放入
            if (p.casNext(null, newNode)) {   
                // 当前p如果不等于t  则更新尾结点,  p!=t是因为当前tail不是尾结点,P重新赋值了头节点导致的P和T不同
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
        }
      // tail未指向尾结点,并且p==q 说明当前tail为闭环,可能当前节点已经舍弃
        else if (p == q)
            // 如果p的next等于p,说明p已经被删除了(已经出队了)
            // 重新设置p的值
            p = (t != (t = tail)) ? t : head;
        else
          // t后面还有值,重新设置p的值
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

快照图

阻塞队里总结与重要源码解析_第4张图片

(5)poll方法
public E poll() {
    restartFromHead:
    for (;;) {
      // p节点表示首节点,即需要出队的节点
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;
            // 如果p节点的元素不为null,则通过CAS来设置p节点引用的元素为null,如果成功则返回p节点的元素
            if (item != null && p.casItem(item, null)) {
               // 如果p != h,则更新head
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
          	// 如果头节点的元素为空或头节点发生了变化,这说明头节点已经被另外一个线程修改了。
            // 那么获取p节点的下一个节点,如果p节点的下一节点为null,则表明队列已经空了
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
           // p == q,则使用新的head重新开始
            else if (p == q)
                continue restartFromHead;
          // 如果下一个元素不为空,则将头节点的下一个节点设置成头节点
            else
                p = q;
        }
    }
}
updateHead
final void updateHead(Node<E> h, Node<E> p) {
    if (h != p && casHead(h, p))
      // 将旧的头结点h的next域指向为h
        h.lazySetNext(h);
}

6.PriorityBlockingQueue详解

(1)PriorityBlockingQueue概述

PriorityBlockingQueue是一个无界的基于数组的优先级阻塞队列,数组的默认长度是11,也可以指定数组的长度,且可以无限的扩充,直到资源消耗尽为止,每次出队都返回优先级别最高的或者最低的元素。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。

(2)PriorityBlockingQueue类图

阻塞队里总结与重要源码解析_第5张图片

(3)PriorityBlockingQueue属性值
//默认容量
private static final int DEFAULT_INITIAL_CAPACITY = 11;

//最大容量设定
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//存放数据的数组
private transient Object[] queue;

//元素个数
private transient int size;

//排序规则(比较器)
private transient Comparator<? super E> comparator;

//独占锁
private final ReentrantLock lock;

//队列为空的时候的阻塞队列
private final Condition notEmpty;

//用于分配的CAS自旋锁
private transient volatile int allocationSpinLock;

//只用于序列化的普通优先队列
private PriorityQueue<E> q;
(4)PriorityBlockingQueue构造函数
//无参构造器
public PriorityBlockingQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

//初始化容量
public PriorityBlockingQueue(int initialCapacity) {
    this(initialCapacity, null);
}

//初始化容量并指定自定义的比较器
public PriorityBlockingQueue(int initialCapacity,
                             Comparator<? super E> comparator) {
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    this.comparator = comparator;
    this.queue = new Object[initialCapacity];
}

//初始化并添加集合
public PriorityBlockingQueue(Collection<? extends E> c) {
    this.lock = new ReentrantLock();
    this.notEmpty = lock.newCondition();
    boolean heapify = true; // true if not known to be in heap order
    boolean screen = true;  // true if must screen for nulls
    if (c instanceof SortedSet<?>) {
        SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
        this.comparator = (Comparator<? super E>) ss.comparator();
        heapify = false;
    }
    else if (c instanceof PriorityBlockingQueue<?>) {
        PriorityBlockingQueue<? extends E> pq =
            (PriorityBlockingQueue<? extends E>) c;
        this.comparator = (Comparator<? super E>) pq.comparator();
        screen = false;
        if (pq.getClass() == PriorityBlockingQueue.class) // exact match
            heapify = false;
    }
    Object[] a = c.toArray();
    int n = a.length;
    // If c.toArray incorrectly doesn't return Object[], copy it.
    if (a.getClass() != Object[].class)
        a = Arrays.copyOf(a, n, Object[].class);
    if (screen && (n == 1 || this.comparator != null)) {
        for (int i = 0; i < n; ++i)
            if (a[i] == null)
                throw new NullPointerException();
    }
    this.queue = a;
    this.size = n;
    if (heapify)
        heapify();
}
(5)put方法源码分析
public void put(E e) {
  //put和offer一样,不需要阻塞
    offer(e); // never need to block
}
PriorityBlockingQueue.offer
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
  //当元素个数大于队列长度则先进行扩容
    while ((n = size) >= (cap = (array = queue).length))
      //执行扩容操作
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
          //使用默认的比较器插入数据
            siftUpComparable(n, e, array);
        else
          //使用自定义的比较器插入数据
            siftUpUsingComparator(n, e, array, cmp);
			//大小+1
        size = n + 1;
      // 当我们入队成功之后,说明队列中已经有元素了,则唤醒被notEmpty阻塞的线程,也就是可以继续执行出队操作了
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}
PriorityBlockingQueue.siftUpComparable
// k为队列中元素的数量,x为要入队的元素,array就是我们的队列
private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
      // 这里拓展一下位运算,<<表示左移移,不分正负数,低位补0
    	// >>表示右移,如果该数为正,则高位补0,若为负数,则高位补1
    	// >>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0
    	// 这里的意思等价于parent = (k - 1) / 2,通过上面的二叉堆特性,我们知道了这个parent其实就是k的父节点
        int parent = (k - 1) >>> 1;
      // 取出队列中间的元素,设置为e
        Object e = array[parent];
      // 如果要入队的元素大于等于e的话,就直接break掉,最后将这个元素加在数组最后即可
        if (key.compareTo((T) e) >= 0)
            break;
      // 如果上面的if条件不满足,则将e与k的位置交换,然后再跟父节点比大小
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

这段代码采用了二叉堆向上调整算法

**二叉堆的概念:**二叉堆是一种特殊的堆,二叉堆是完全二元树(二叉树)或者是近似完全二元树(二叉树)。二叉堆有两种:最大堆和最小堆。最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。

举例1:数组[1,2,3,4,5,6]中添加7

阻塞队里总结与重要源码解析_第6张图片

通过(n - 1) / 2找到父节点的值是3,发现 7 > 3,那么直接将7加在数组尾部即可

举例2:数组[1,2,4,5,6,7]中添加3

阻塞队里总结与重要源码解析_第7张图片

通过(n - 1) / 2找到父节点的值是3,发现 3 > 4,此时就将3和4的位置进行调换

PriorityBlockingQueue.siftUpComparable
//与PriorityBlockingQueue.siftUpComparable几乎一模一样,仅仅是比较器不一样
private static <T> void siftUpUsingComparator(
    int k, T x, Object[] es, Comparator<? super T> cmp) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = es[parent];
        if (cmp.compare(x, (T) e) >= 0)
            break;
        es[k] = e;
        k = parent;
    }
    es[k] = x;
}
PriorityBlockingQueue.tryGrow
private void tryGrow(Object[] array, int oldCap) {
  //必须释放然后重新获得主锁,这一步的意义在于所有操作共享一把锁,在进行扩容时(因为写已满),释放锁(不能写,要等待扩容完才能写),提供给读操作
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
  // CAS 轻量级锁加锁,避免并发扩容;加锁操作就是CAS将0变为1
    if (allocationSpinLock == 0 &&
        ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
        try {
          // 扩容步长,旧值小于64时,变为两倍+2。大于64时,变为1.5倍。
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
          // 超过最大容量,可能内存溢出
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
              //+1后判断是否已经内存溢出
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
              //如果+1没有内存溢出,直接设置为最大值,最大值肯定小于1.5xoldCap
                newCap = MAX_ARRAY_SIZE;
            }
          // 创建新数组
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
          //释放锁,1修改为0
            allocationSpinLock = 0;
        }
    }
  // 并发扩容时,线程让出 cpu 执行时间,给其他线程执行,自己稍后执行,原因:加锁不成功必然有其他线程也在扩容,在等待过程中让出资源给其他线程利用
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
  //重新加锁
    lock.lock();
  //变更内存指向,利用内存拷贝复制旧数据
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}
(6)take方法源码分析
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
  //获取锁,考虑中断
    lock.lockInterruptibly();
    E result;
    try {
      //如果队列为空,等待;队列不为空,执行出队操作
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
      //释放锁
        lock.unlock();
    }
    return result;
}
PriorityBlockingQueue.dequeue
private E dequeue() {
    int n = size - 1;
  //n<0说明队列为空
    if (n < 0)
        return null;
    else {
        Object[] array = queue;
      //第一个始终是最大或者最小的对象
        E result = (E) array[0];
        E x = (E) array[n];
      //将数据最后一个数据设为空
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
          //使用默认比较器进行数据调整
            siftDownComparable(0, x, array, n);
        else
          //使用自定义的比较器进行数据调整
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}
PriorityBlockingQueue.siftDownUsingComparator
//在dequeue方法中,k是要出队的数据索引0(即出队的元素),x是数组最后一个数据,n是数组最后一个数据的索引
private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
      //n/2
        int half = n >>> 1;           // loop while a non-leaf
      //在数组中,位置K的节点的子节点的下标肯定小于half
        while (k < half) {
          //k<<1即 kx2  获取k节点的左子节点
            int child = (k << 1) + 1; // assume left child is least
          //获取左子节点对应的值
            Object c = array[child];
          //获取k节点右子节点的索引
            int right = child + 1;
          //比较右节点的索引是否小于队列长度和左右子节点的值进行比较 
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
              //如果左子节点大于右子节点,c取较小的子节点
                c = array[child = right];
          //key小于等于子节点
            if (key.compareTo((T) c) <= 0)
              //结束
                break;
          //如果key大于子节点,交换位置,将较小的子节点的值放到父结点K的位置
            array[k] = c;
          //此时需要比较和交换的目标位置变成了child,在while循环中继续向下比较
            k = child;
        }
      //将最后一个元素的值放在K这个位置
        array[k] = key;
    }
}
PriorityBlockingQueue.siftDownUsingComparator
//与PriorityBlockingQueue.siftDownComparable几乎一模一样,仅仅是比较器不一样
private static <T> void siftDownUsingComparator(int k, T x, Object[] array,
                                                int n,
                                                Comparator<? super T> cmp) {
    if (n > 0) {
        int half = n >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = array[child];
            int right = child + 1;
            if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                c = array[child = right];
            if (cmp.compare(x, (T) c) <= 0)
                break;
            array[k] = c;
            k = child;
        }
        array[k] = x;
    }
}
(7)PriorityBlockingQueue实际使用场景
  • 抢购活动,会员级别高的用户优先抢购到商品
  • 银行办理业务,vip客户插队

7.DelayQueue详解

(1)DelayQueue概述

DelayQueue 是一个通过PriorityBlockingQueue实现延迟获取元素的无界阻塞队列,其中添加进该队列的元素必须实现Delayed接口(指定延迟时间),而且只有在延迟期满后才能从中提取元素。注意:不能将null元素放置到这种队列中

(2)DelayQueue类图

阻塞队里总结与重要源码解析_第8张图片

(3)DelayQueue属性值
//用于控制并发的锁
private final transient ReentrantLock lock = new ReentrantLock();
//优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
//用于标记当前是否有线程在排队(仅用于取元素时)
private Thread leader = null;
//条件,用于表示现在是否有可取的元素
private final Condition available = lock.newCondition();
(4)DelayQueue构造函数
public DelayQueue() {}

public DelayQueue(Collection<? extends E> c) {
    this.addAll(c);
}
(5)put方法源码分析
public void put(E e) {
    offer(e);
}
DelayQueue.offer
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
  //加锁
    lock.lock();
    try {
      //调用PriorityQueue的offer方法,源码参考PriorityBlockingQueue,类似
        q.offer(e);
			// 如果队列的第一个元素是插入的元素(插入成功),则设置 leader=null,并且通知等待锁的线程。
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}
(6)take方法源码分析
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
          // 堆顶元素
            E first = q.peek();
          // 如果堆顶元素为空,说明队列中还没有元素,直接阻塞等待
            if (first == null)
                available.await();
            else {
              // 堆顶元素的到期时间
                long delay = first.getDelay(NANOSECONDS);
              // 如果小于0说明已到期,直接调用poll()方法弹出堆顶元素
                if (delay <= 0)
                    return q.poll();
              
              //如果delay大于0  下面要开始阻塞了
                first = null; // don't retain ref while waiting
              // 如果前面有其它线程在等待,直接进入等待
                if (leader != null)
                    available.await();
                else {
                  // 如果leader为null,把当前线程赋值给它
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待delay时间后自动醒过来
                        // 醒过来后把leader置空并重新进入循环判断堆顶元素是否到期
                        // 这里即使醒过来后也不一定能获取到元素
                        // 因为有可能其它线程先一步获取了锁并弹出了堆顶元素
                        // 条件锁的唤醒分成两步,先从Condition的队列里出队
                        // 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
                        available.awaitNanos(delay);
                    } finally {
                       // 如果leader还是当前线程就把它置为空,让其它线程有机会获取元素
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
       // 成功出队后,如果leader为空且堆顶还有元素,就唤醒下一个等待的线程
        if (leader == null && q.peek() != null)
          //signal()只是把等待的线程放到AQS的队列里面,并不是真正的唤醒
            available.signal();
      //解锁,这才是真正的唤醒
        lock.unlock();
    }
}
(7)DelayQueue使用场景
  • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 订单延迟支付关闭。(MQ延迟消息队列/时间轮/定时任务)

你可能感兴趣的:(Java面试,java,网络,开发语言,面试)