文章目录
一、:BlockingQueue简介
在常见的"生产者-消费者"问题中,队列通常被视作线程间操作数据的容器,这样可以实现对各个模块的业务功能进行解耦。生产者将数据放置在容器中,而消费者仅需要在容器中进行获取即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能。
阻塞队列(BlockingQueue)被广泛应用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
常见的BlockingQueue
实现BlockingQueue接口的有ArrayBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue、LinkedTransferQueue、LinkedBlockingDeque、LinkedBlockingQueue,而这几种常见的阻塞队列也是在实际编程中常用的。
二、:ArrayBlockingQueue
①ArrayBlockingQueue简介
ArrayBlockingQueue是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者存入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。
当队列容量满时,尝试将元素放入队列将导致操作阻塞。当尝试从一个空队列中取一个元素同样会阻塞。
ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。(如果保证公平性,通常会降低吞吐量)
②ArrayBlockingQueue的关键属性
// 使用数组存储元素
final Object[] items;
// 取元素的指针
int takeIndex;
// 存元素的指针
int putIndex;
// 元素数量
int count;
// 锁
final ReentrantLock lock;
// 非空条件
private final Condition notEmpty;
// 非满条件
private final Condition notFull;
通过观察属性,我们可以看到几个重要信息:
●使用数组存储元素。
●通过存指针和取指针来标记下一次操作的位置。
●利用ReentrantLock来保证并发安全。
●利用Condition作为多线程中消息通知机制。(当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当存入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中)
③ArrayBlockingQueue的重点方法分析
1、构造方法
// 指定容量,默认非公平
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
// 指定容量,指定是否公平
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 初始化数组
this.items = new Object[capacity];
// 创建锁及两个Condition
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
// 指定容量,指定是否公平,使用指定集合内容
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
2、存数据方法
public boolean add(E e) {
// 调用父类的add()方法
return super.add(e);
}
// super.add()
public boolean add(E e) {
// 调用offer()方法,如果成功返回true,如果失败抛出异常
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
public boolean offer(E e) {
// 存入元素不可为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 如果数组满了就返回false
if (count == items.length)
return false;
// 如果数组没满就调用入队方法并返回true
else {
enqueue(e);
return true;
}
} finally {
// 解锁
lock.unlock();
}
}
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 = 0;
// 数量加1
count++;
// 唤醒notEmpty(因为入队了一个元素,所以肯定不为空了)
notEmpty.signal();
}
public void put(E e) throws InterruptedException {
// 存入元素不可为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁,如果线程中断了抛出异常
lock.lockInterruptibly();
try {
// 如果数组满了,使用notFull等待(这里之所以使用while而不是if,是考虑到多线程并发问题)
while (count == items.length)
notFull.await();
// 入队
enqueue(e);
} finally {
// 解锁
lock.unlock();
}
}
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) {
// 如果超时就返回false
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
// 入队
enqueue(e);
return true;
} finally {
// 解锁
lock.unlock();
}
}
从上面诸多方法可以看出,存入队列的数据不能为空。每个方法对应的作用如下:
●add(E e):将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回true。如果此队列已满,则抛出IllegalStateException异常。
●offer(E e):将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量),在成功时返回true。如果此队列已满,则返回false。
●put(E e):将指定的元素插入此队列的尾部,如果该队列已满,则等待可用的空间(阻塞)。
●offer(E e, long timeout, TimeUnit unit):将指定的元素插入此队列的尾部,如果该队列已满,则在到达指定的等待时间之前等待可用的空间。超时则返回false。
虽然方法比较简单易懂,但是细节之处无不体现出严谨与巧妙。
3、取数据方法
public E poll() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 如果队列没有元素则返回null,否则出队
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];
// 把取指针位置设为null
items[takeIndex] = null;
// 取指针前移,如果到头了就返回数组前端循环利用
if (++takeIndex == items.length)
takeIndex = 0;
// 元素数量减1
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒notFull(因为取出了一个元素,所以肯定不满了)
notFull.signal();
return x;
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
// 加锁,如果线程中断了抛出异常
lock.lockInterruptibly();
try {
// 如果队列无元素,则阻塞等待nanos纳秒
while (count == 0) {
// 如果超时就返回null
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
// 出队
return dequeue();
} finally {
// 解锁
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁,如果线程中断了抛出异常
lock.lockInterruptibly();
try {
// 如果队列无元素,则阻塞等待在条件notEmpty上
while (count == 0)
notEmpty.await();
// 出队
return dequeue();
} finally {
// 解锁
lock.unlock();
}
}
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);
}
return false;
} finally {
// 解锁
lock.unlock();
}
}
每个方法对应的作用如下:
●poll():获取并移除此队列的头部,如果此队列为空,则返回null。
●poll(long timeout, TimeUnit unit):获取并移除此队列的头部,在指定的等待时间前等待可用的元素。
●take():获取并移除此队列的头部,在元素变得可用之前一直等待。
●remove(Object o):从此队列中移除指定元素的单个实例(如果存在)。
④总结
●ArrayBlockingQueue不需要扩容,因为是初始化时指定容量,并循环利用数组。
●ArrayBlockingQueue利用takeIndex和putIndex循环利用数组。
●入队和出队各定义了四种方法满足不同的用途。
●利用ReentrantLock和两个Condition保证并发安全。
三、:PriorityBlockingQueue
①PriorityBlockingQueue简介
一般队列都是采用FIFO原则来确定线程执行的先后顺序。而PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。
PriorityBlockingQueue底层是基于数组实现的堆结构。
②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更新这个值,谁更新成功了谁扩容,其它线程让出CPU
private transient volatile int allocationSpinLock;
// 不阻塞的优先级队列,非存储元素的地方,仅用于序列化/反序列化时
private PriorityQueue<E> q;
根据属性可以看出,也是利用ReentrantLock来保证并发安全。相比上面的ArrayBlockingQueue,少了notFull属性,也说明PriorityBlockingQueue是无界队列。
③PriorityBlockingQueue的重点方法分析
1、构造方法
// 默认容量为11
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();
// 初始化Condition
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();
}
2、存数据方法
public boolean add(E e) {
return offer(e);
}
public void put(E e) {
offer(e); // never need to block
}
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;
}
从上面的方法可以看出,存入队列的数据不能为空。内部方法执行流程如下:
●加锁。
●判断是否需要扩容。
●添加元素并做自下而上的堆化。
●元素个数加1并唤醒notEmpty,唤醒取元素的线程。
●解锁。
ⅰ、tryGrow()
private void tryGrow(Object[] array, int oldCap) {
// 解锁(外部offer()方法加的锁),通过allocationSpinLock变量控制扩容的过程,防止阻塞的线程过多
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// CAS更新allocationSpinLock变量为1的线程获得扩容资格
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// 旧容量小于64则翻倍,旧容量大于64则增加一半
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
// 判断新容量是否溢出
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
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
allocationSpinLock = 0;
}
}
// 未执行上面的扩容操作进入这里
if (newArray == null) // back off if another thread is allocating
// 让不执行扩容的线程让出CPU
Thread.yield();
// 再次加锁
lock.lock();
// 当新数组创建成功并且旧数组没有被替换过
if (newArray != null && queue == array) {
// 队列赋值为新数组
queue = newArray;
// 拷贝旧数组元素到新数组中
System.arraycopy(array, 0, newArray, 0, oldCap);
}
}
代码主要流程如下:
●解锁(解除offer()方法中加的锁)。
●使用allocationSpinLock变量的CAS操作来控制扩容的过程。
●旧容量小于64则翻倍,旧容量大于64则增加一半。
●创建新数组。
●allocationSpinLock的值置0,相当于解锁控制扩容。
●其它线程在扩容的过程中要让出CPU。
●再次加锁。
●新数组创建成功,把旧数组元素拷贝过来,并返回到offer()方法中继续添加元素操作。
ⅱ、siftUpComparable()
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
// 取父节点脚标
int parent = (k - 1) >>> 1;
// 取父节点的元素值
Object e = array[parent];
// 如果key大于父节点,堆化结束
if (key.compareTo((T) e) >= 0)
break;
// 否则,交换二者的位置,继续下一轮比较
array[k] = e;
k = parent;
}
// 找到了应该放的位置,放入元素
array[k] = key;
}
使用默认的比较器执行插入元素操作。(自下而上堆化)
ⅲ、siftUpUsingComparator()
private static <T> void siftUpUsingComparator(int k, T x, Object[] array, Comparator<? super T> cmp) {
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (cmp.compare(x, (T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = x;
}
使用指定的比较器执行插入元素操作。(自下而上堆化)
3、取数据方法
public E poll() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
return dequeue();
} finally {
// 解锁
lock.unlock();
}
}
private E dequeue() {
// 元素个数减1
int n = size - 1;
// 数组无元素,返回null
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
size = n;
return result;
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ((result = dequeue()) == null && nanos > 0)
nanos = notEmpty.awaitNanos(nanos);
} finally {
lock.unlock();
}
return result;
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
E result;
try {
// 如果队列没有元素,就阻塞在notEmpty上;如果出队成功,就跳出这个循环
while ((result = dequeue()) == null)
notEmpty.await();
} finally {
// 解锁
lock.unlock();
}
return result;
}
以take()方法为例,内部方法执行流程如下:
●加锁。
●判断是否出队成功,未成功就阻塞在notEmpty上。
●出队时弹出堆顶元素,并对剩余元素做自上而下的堆化。
●解锁。
ⅰ、siftDownComparable()
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
// 最后一个叶子节点的父节点位置
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
// 待调整位置左节点位置
int child = (k << 1) + 1; // assume left child is least
// 左节点
Object c = array[child];
// 右节点
int right = child + 1;
// 左右节点比较,取较小的
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
// 如果待调整key最小,那就退出,直接赋值
if (key.compareTo((T) c) <= 0)
break;
// 如果key不是最小,那就取左右节点小的那个放到调整位置,然后小的那个节点位置开始再继续调整
array[k] = c;
k = child;
}
array[k] = key;
}
}
使用默认的比较器执行取出元素操作。(自上而下堆化)
ⅱ、siftDownUsingComparator()
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;
}
}
使用指定的比较器执行取出元素操作。(自上而下堆化)
④总结
●PriorityBlockingQueue使用ReentrantLock和notEmpty的Condition控制并发安全。
●PriorityBlockingQueue扩容时使用一个单独变量的CAS操作来控制只有一个线程进行扩容。
●入队使用自下而上的堆化。
●出队使用自上而下的堆化。
四、:DelayQueue
①DelayQueue简介
DelayQueue是一个支持延时获取元素的无界阻塞队列。里面的元素全部都是“可延期”的元素,列头的元素是最先“到期”的元素;如果队列里面没有元素“到期”,是不能从列头获取元素的。也就是说只有在达到延迟期时才能够从队列中取元素。
DelayQueue主要用于两个方面:
●清除缓存中超时的数据。
●任务超时处理。
②DelayQueue的关键属性
// 锁
private final transient ReentrantLock lock = new ReentrantLock();
// 优先级队列(根据Delay时间排序)
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 取元素时,用于标记当前是否有线程在排队(优化阻塞)
private Thread leader = null;
// Condition(用于表示现在是否有可取的元素)
private final Condition available = lock.newCondition();
根据属性参数,可以猜测DelayQueue的内部实现机制为:以支持优先级无界队列的PriorityQueue作为一个容器,容器里面的元素都应该实现Delayed接口,在每次往优先级队列中添加元素时以元素的过期时间作为排序条件,最先过期的元素放在优先级最高。
③DelayQueue的重点方法分析
1、构造方法
public DelayQueue() {}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
if (c == null)
throw new NullPointerException();
if (c == this)
throw new IllegalArgumentException();
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
2、存数据方法
public void put(E e) {
offer(e);
}
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 向PriorityQueue中插入元素
q.offer(e);
// 如果当前元素是队首元素(优先级最高),leader设置为空,唤醒所有等待线程
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
// 解锁
lock.unlock();
}
}
入队方法执行流程如下:
●加锁。
●添加元素到PriorityQueue中。
●如果添加的元素是队首元素,就把leader置为空,并唤醒等待在条件available上的线程。
●解锁。
入队方法比较简单,但是在判断当前元素是否为对首元素,如果是的话则把leader置为空,这是性能优化的一个步骤,代表取操作未被线程占用。
3、取数据方法
ⅰ、poll()
public E poll() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
E first = q.peek();
// 队首元素如果为空或者还未到期,就返回null;否则出队返回对应元素
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
// 解锁
lock.unlock();
}
}
方法执行流程如下:
●加锁。
●检查队首元素,如果为空或者还没到期,就返回null。
●如果队首元素到期了,就调用PriorityQueue的poll()方法取出第一个元素。
●解锁。
ⅱ、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);
// 如果已到期,出队返回对应元素
if (delay <= 0)
return q.poll();
// help GC,防止内存泄漏
first = null; // don't retain ref while waiting
// 如果有其他线程在操作,阻塞等待
if (leader != null)
available.await();
else {
// 将当前线程设置为leader(独占)
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 超时阻塞(到delay时间自动唤醒)
available.awaitNanos(delay);
} finally {
// 释放leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 成功出队后,如果leader为空且队中还有元素,就唤醒下一个等待的线程
if (leader == null && q.peek() != null)
// signal()只是把等待的线程放到AQS的队列里面,并不是真正的唤醒
available.signal();
// 解锁
lock.unlock();
}
}
方法执行流程如下:
●加锁。
●判断队首元素是否为空,为空的话直接阻塞等待。
●判断队首元素是否到期,到期了直接调用PriorityQueue的poll()方法取出元素。
●如果队首元素不为空且没到期,再判断是否有其它线程占用资源,有则直接等待。
●如果没有其它线程占用资源,则把自己当作第一个线程,等待delay时间后唤醒,再尝试获取元素。
●获取到元素之后再唤醒下一个等待的线程。
●解锁。
关于设置first = null防止内存泄漏的解释:
假如有线程A进入,设置leader = 线程A。此时线程B也来了,因为leader != null,则会阻塞。线程C、D、E……同理。假如线程A成功将队首元素出列,之后队首元素应该会被回收掉。但是它还被线程B、线程C等等持有着,所以不会被回收。如果线程无限多,就会造成内存泄漏。
④总结
●DelayQueue是阻塞队列,内部存储结构使用PriorityQueue。
●DelayQueue使用ReentrantLock和Condition来控制并发安全。
●DelayQueue常用于定时任务。
五、:SynchronousQueue
①SynchronousQueue简介
SynchronousQueue作为BlockingQueue大家庭中的一员,与其他BlockingQueue有着与众不同的特性:
●SynchronousQueue没有容量。SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
●因为没有容量,所以对应peek()、contains()、clear()、isEmpty()等方法其实是无效的。例如:clear是不执行任何操作的,contains始终返回false,peek始终返回null。
●SynchronousQueue分为公平和非公平,默认情况下采用非公平访问策略。
●若使用TransferQueue,则队列中永远会存在一个dummy node。
综合以上特性,SynchronousQueue非常适合做交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。
②SynchronousQueue的关键属性及类
1、关键属性
// CPU数量
static final int NCPUS = Runtime.getRuntime().availableProcessors();
// 有超时的情况下自旋次数(当CPU数量小于2时不自旋)
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
// 没有超时的情况下自旋次数
static final int maxUntimedSpins = maxTimedSpins * 16;
// 针对有超时的情况,自旋了多次后,如果剩余时间大于1000纳秒就使用带时间的LockSupport.parkNanos()方法
static final long spinForTimeoutThreshold = 1000L;
// 传输器(即两个线程交换元素使用的东西)
private transient volatile Transferer<E> transferer;
通过观察属性,我们可以发现以下内容:
●这个阻塞队列里面是会有自旋操作的。
●内部使用了一个叫做transferer的东西来交换元素。
2、关键类
// Transferer抽象类,包含一个transfer()方法用来传输元素
abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}
// 以栈方式实现的Transferer
static final class TransferStack<E> extends Transferer<E> {
// 栈中节点的几种类型:
// 消费者(请求数据的)
static final int REQUEST = 0;
// 生产者(提供数据的)
static final int DATA = 1;
// 二者正在匹配中
static final int FULFILLING = 2;
/** Node class for TransferStacks. */
static final class SNode {
// 下一个节点
volatile SNode next; // next node in stack
// 匹配者
volatile SNode match; // the node matched to this
// 等待着的线程
volatile Thread waiter; // to control park/unpark
// 元素
Object item; // data; or null for REQUESTs
// 节点类型(对应上面的)
int mode;
}
// 栈的头节点
volatile SNode head;
}
// 以队列方式实现的Transferer
static final class TransferQueue<E> extends Transferer<E> {
// 队列中的节点
static final class QNode {
// 下一个节点
volatile QNode next; // next node in queue
// 元素
volatile Object item; // CAS'ed to or from null
// 等待着的线程
volatile Thread waiter; // to control park/unpark
// 是否是数据节点
final boolean isData;
}
// 队列的头节点
transient volatile QNode head;
// 队列的尾节点
transient volatile QNode tail;
}
通过观察类,我们可以发现以下内容:
●定义了一个抽象类Transferer,内含一个传输元素的方法。
●有两种实现类,一种是栈(LIFO),一种是队列(FIFO)。
●栈只需要保存一个头节点(存取元素都是操作头节点)。
●队列需要保存一个头节点和一个尾节点(存元素操作尾节点,取元素操作头节点)。
●每个节点中都保存着存储的元素、等待着的线程,以及下一个节点。
③SynchronousQueue的重点方法分析
1、构造方法
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
通过构造方法,我们可以看到前面有栈和队列两种实现的原因:栈结构的结果是实现非公平非公平模式,队列结构的结果是实现公平模式。
2、存数据方法
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();
}
}
从方法中可以直观的看到,存入的数据不能为空。
3、取数据方法
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
// 如果取到了元素就返回
if (e != null)
return e;
// 如果未取到元素,让线程中断并抛出中断异常
Thread.interrupted();
throw new InterruptedException();
}
4、TransferStack的transfer()方法
从上面可以看出,存取数据时都调用了transfer()方法,因此核心逻辑在这里体现。以TransferStack中的transfer()方法为例:
// 入参依次是:传输的元素,是否需要超时,超时的时间
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
// 根据e是否为null判断是生产者(存操作)还是消费者(取操作)
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
// 获取栈顶
SNode h = head;
// 栈顶为空,或者栈顶模式跟当前模式一致
if (h == null || h.mode == mode) { // empty or same-mode
// 如果有超时而且已到期
if (timed && nanos <= 0) { // can't wait
// 如果栈顶不为空且是取消状态,弹出栈顶(进入下一次循环)
if (h != null && h.isCancelled())
casHead(h, h.next); // pop cancelled node
// 如果栈顶为空或不是取消状态,直接返回null
else
return null;
// 尝试入栈成功
} else if (casHead(h, s = snode(s, e, h, mode))) {
// 自旋+阻塞当前入栈的线程,并等待被匹配到
SNode m = awaitFulfill(s, timed, nanos);
// 如果m等于s,说明取消了,那么就把它清除掉,并返回null
if (m == s) { // wait was cancelled
clean(s);
return null;
}
// 到这里说明匹配到元素了(经过awaitFulfill()方法,要么被取消了,要么匹配)
// 如果栈顶不为空,并且栈顶的下一个节点是s,就把栈顶换成s的下一个节点(弹出h和s)
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
// 根据当前节点的模式判断返回m还是s中的值
return (E) ((mode == REQUEST) ? m.item : s.item);
}
// 到这里说明栈顶模式和当前模式不一样
// 如果栈顶不是正在匹配中
} else if (!isFulfilling(h.mode)) { // try to fulfill
// 如果栈顶已经取消了,就把它弹出
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
// 栈顶未取消,就让当前节点先入栈,再让他们尝试匹配(s成为了新的栈顶,它的状态是FULFILLING)
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
// 如果m为null(除s节点外的节点都被其它线程抢先匹配了),就清空栈并跳出内部循环,到外部循环再重新入栈判断
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
// 如果m和s尝试匹配成功,就弹出栈顶的两个元素m和s
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
// 尝试匹配失败(m已经被其它线程抢先匹配了),协助清除它
} else // lost match
s.casNext(m, mn); // help unlink
}
}
// 栈顶模式匹配中
} else { // help a fulfiller
SNode m = h.next; // m is h's match
// 如果m为null(m已经被其它线程抢先匹配了),弹出已匹配的节点
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
// 如果m和h尝试匹配成功,就弹出栈顶的两个元素m和h
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
// 尝试匹配失败(m已经被其它线程抢先匹配了),协助清除它
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
代码整体量比较多,逻辑也不少,其主要执行流程是:
●如果栈中没有元素,或者栈顶模式跟将要入栈的元素模式一样,就入栈。
●入栈后自旋等待一会看有没有其它线程匹配到它,自旋完了还没匹配到元素就阻塞等待。
●阻塞等待被唤醒了说明其它线程匹配到了当前元素,就返回匹配到的元素。
●如果两者模式不一样,且栈顶未在匹配中,就拿当前节点尝试匹配,匹配成功了就返回匹配到的元素。
●如果两者模式不一样,且栈顶正在匹配中,当前线程就协助去匹配,匹配完成了再让当前节点重新入栈重新匹配。
ⅰ、awaitFulfill()
// 入参依次是:需要等待的节点,是否需要超时,超时的时间
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
// 获取到期时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
// 自旋次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 当前线程中断了,尝试清除s
if (w.isInterrupted())
s.tryCancel();
// 检查s是否匹配到了元素m(也有可能是其它线程的m匹配到当前线程的s)
SNode m = s.match;
// 如果匹配到了,直接返回m
if (m != null)
return m;
// 如果需要超时,判断超时时间
if (timed) {
nanos = deadline - System.nanoTime();
// 如果已超时,尝试清除s
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
// 如果还有自旋次数,自旋次数减一,并进入下一次自旋
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
// 后面都是自旋次数没有了的情况
// 如果s的waiter为null,把当前线程注入进去,并进入下一次自旋
else if (s.waiter == null)
s.waiter = w; // establish waiter so can park next iter
// 如果不允许超时,直接阻塞,并等待被其它线程唤醒,唤醒后继续自旋并查看是否匹配到了元素
else if (!timed)
LockSupport.park(this);
// 如果允许超时且还有剩余时间,就阻塞相应时间
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
ⅱ、tryMatch()
// 从上面代码看到,是m调用的tryMatch()方法
boolean tryMatch(SNode s) {
// 如果m还没有匹配者,就把s作为它的匹配者
if (match == null &&
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
Thread w = waiter;
// 唤醒m中的线程,两者匹配完毕
if (w != null) { // waiters need at most one unpark
waiter = null;
LockSupport.unpark(w);
}
return true;
}
// 也可能其它线程先一步匹配了m,返回最终匹配到的是否是s
return match == s;
}
5、TransferQueue的transfer()方法
看完TransferStack中的transfer()方法,其实TransferQueue中的transfer()方法思想一致:
E transfer(E e, boolean timed, long nanos) {
QNode s = null; // constructed/reused as needed
// 当前节点是否是数据节点(模式)
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
// 头/尾节点为null(没有初始化),自旋等待
if (t == null || h == null) // saw uninitialized value
continue; // spin
// 头尾节点相等(队列为null)或者当前节点和队列节点模式一样
if (h == t || t.isData == isData) { // empty or same-mode
QNode tn = t.next;
// t != tail(已有其他线程操作了,修改了tail),重新循环
if (t != tail) // inconsistent read
continue;
// tn != null(已经有其他线程添加了节点),tn推进,重新循环
if (tn != null) { // lagging tail
// 当前线程帮忙推进尾节点(尝试将tn设置为尾节点)
advanceTail(t, tn);
continue;
}
// 需要限时并且已经超时,直接返回null
if (timed && nanos <= 0) // can't wait
return null;
// 如果s == null,构建一个新节点Node
if (s == null)
s = new QNode(e, isData);
// 将新建的节点加入到队列中,如果不成功,重新循环
if (!t.casNext(null, s)) // failed to link in
continue;
// 替换尾节点
advanceTail(t, s); // swing tail and wait
// 自旋+阻塞当前线程,并等待被匹配到
Object x = awaitFulfill(s, e, timed, nanos);
// x == s(当前线程已经超时或者中断)
if (x == s) { // wait was cancelled
// 清理节点s
clean(t, s);
return null;
}
// 判断节点是否已经从队列中离开
if (!s.isOffList()) { // not already unlinked
// 尝试将s节点设置为head,移出t
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
// 释放等待线程
s.waiter = null;
}
return (x != null) ? (E)x : e;
// 队列不为null 且 当前节点和队列节点模式不一样
} else { // complementary-mode
QNode m = h.next; // node to fulfill
// 不一致读(有其他线程做了更改)
if (t != tail || m == null || h != head)
continue; // inconsistent read
// 生产者producer和消费者consumer匹配
Object x = m.item;
// isData == (x != null)(isData与x的模式相同,表示已经匹配了)
if (isData == (x != null) || // m already fulfilled
// x == m(m节点被取消了)
x == m || // m cancelled
// !m.casItem(x, e)(尝试将数据e设置到m上失败)
!m.casItem(x, e)) { // lost CAS
// 将m设置为头结点,h出队,重新循环
advanceHead(h, m); // dequeue and retry
continue;
}
// 成功匹配之后,m设置为头结点,h出队,向前推进
advanceHead(h, m); // successfully fulfilled
// 唤醒m上的等待线程
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
代码相比TransferStack中的transfer()方法较为简单,其主要执行流程是:
●如果队列为null或者尾节点模式与当前节点模式一致,则尝试将节点加入到等待队列中(采用自旋的方式),直到被匹配、超时或者被取消。匹配成功的话要么返回null(生产者返回的),要么返回真正传递的值(消费者返回的),如果返回的是node节点本身则表示当前线程超时或者被取消了。
●如果队列不为null,且队列的节点是当前节点匹配的节点,则进行数据的传递匹配并返回匹配节点的数据。
●整个过程中都会检测并帮助其他线程推进。
ⅰ、awaitFulfill()
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
/* Same idea as TransferStack.awaitFulfill */
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int spins = ((head.next == s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel(e);
Object x = s.item;
if (x != e)
return x;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel(e);
continue;
}
}
if (spins > 0)
--spins;
else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
④总结
●SynchronousQueue是无缓冲队列,用于在两个线程之间直接移交元素。
●SynchronousQueue有两种实现方式,一种是公平方式(队列),一种是非公平方式(栈)。
●栈方式中的节点有三种模式:生产者、消费者、正在匹配中。
●栈方式的大致思路是:如果栈顶模式跟当前节点模式一致就入栈并等待被匹配,否则就匹配,匹配到了就返回。
●队列方式中的节点模式分为:数据节点和非数据节点。
●队列方式的大致思路是:如果队列为null或者尾节点模式与当前节点模式一致就入队并等待被匹配,否则就匹配,匹配到了就返回。
SynchronousQueue是严格意义上的无缓冲队列吗?
通过源码分析,我们发现其实在SynchronousQueue内部,或使用栈或使用队列来存储包含线程和元素值的节点,如果同一模式的节点过多的话,它们都会存储进来,且都会阻塞着,所以,严格上来说,SynchronousQueue并不能算是一个无缓冲队列。
同时,引出一个SynchronousQueue的缺点:
如果生产者速度经常大于消费者速度,生产者则易发生阻塞。因此,SynchronousQueue一般用于生产、消费的速度大致相当的情况,这样才不会导致系统中过多的线程处于阻塞状态。
六、:LinkedTransferQueue
①LinkedTransferQueue简介
大多数BlockingQueue对读或者写都是锁上整个队列,在并发量大的时候并不适用,而上面的SynchronousQueue虽然不会锁住整个队列,但它又没有容量。那么有没有既有容量又不会锁住整个队列的实现呢?这里就引出了LinkedTransferQueue。
LinkedTransferQueue是基于链表的FIFO无界阻塞队列。Doug Lea大神说LinkedTransferQueue是一个聪明的队列。它是ConcurrentLinkedQueue、SynchronousQueue(公平模式)、无界的LinkedBlockingQueues等的超集。
LinkedTransferQueue采用一种预占模式。即消费者线程到队列中取元素时,如果发现队列为空,则会生成一个null节点,然后调用park()方法等待生产者。后面如果生产者线程入队时发现有一个null元素节点,这时生产者就不会入队了,直接将元素填充到该节点上,唤醒该节点的消费者线程,被唤醒的消费者线程取到元素返回。
②LinkedTransferQueue的关键属性及类
1、关键属性
// 是否为多核
private static final boolean MP =
Runtime.getRuntime().availableProcessors() > 1;
// 自旋次数
private static final int FRONT_SPINS = 1 << 7;
// 前驱节点正在处理,当前节点需要自旋的次数
private static final int CHAINED_SPINS = FRONT_SPINS >>> 1;
// 删除节点失败的最大次数
static final int SWEEP_THRESHOLD = 32;
// 头节点
transient volatile Node head;
// 尾节点
private transient volatile Node tail;
// 删除节点失败的次数
private transient volatile int sweepVotes;
// xfer()方法操作参数
private static final int NOW = 0;
private static final int ASYNC = 1;
private static final int SYNC = 2;
private static final int TIMED = 3;
2、关键类
static final class Node {
// 节点是否为数据节点(区分是存数据还是取数据)
final boolean isData; // false if this is a request node
// 存放的数据
volatile Object item; // initially non-null if isData; CASed to match
// 指向下一个节点
volatile Node next;
// 调用了park()方法的线程
volatile Thread waiter; // null until waiting
}
通过isData的值,区分是存操作(isData为false)还是取操作(isData为true)。
③LinkedTransferQueue的重点方法分析
1、构造方法
public LinkedTransferQueue() {
}
public LinkedTransferQueue(Collection<? extends E> c) {
this();
addAll(c);
}
通过构造方法也可以看出,没有设置初始容量的变量,因此是无界队列。
2、存数据方法
public void put(E e) {
xfer(e, true, ASYNC, 0);
}
public boolean add(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
public boolean offer(E e) {
xfer(e, true, ASYNC, 0);
return true;
}
public boolean offer(E e, long timeout, TimeUnit unit) {
xfer(e, true, ASYNC, 0);
return true;
}
存数据的几个方法实现都是一样的,使用ASYNC的方式调用xfer()方法,传入的参数都一模一样。(设置timeout的地方是什么鬼?)
3、取数据方法
public E poll() {
return xfer(null, false, NOW, 0);
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E e = xfer(null, false, TIMED, unit.toNanos(timeout));
if (e != null || !Thread.interrupted())
return e;
throw new InterruptedException();
}
public E take() throws InterruptedException {
E e = xfer(null, false, SYNC, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
这里也是调用xfer()方法,但跟存数据不同的是,各自传入的操作参数不同,poll()方法传入的是NOW和TIMED,take()方法传入的是SYNC。
4、xfer()方法
由上面可以看出,无论存取都是调用的xfer()方法,可见此方法为核心方法。
// 入参依次是:操作元素,是否是数据节点,上面提到的操作参数,超时时间
private E xfer(E e, boolean haveData, int how, long nanos) {
// 存入元素不可为空
if (haveData && (e == null))
throw new NullPointerException();
Node s = null; // the node to append, if needed
retry:
for (;;) { // restart on append race
// 从头节点开始尝试匹配,直到匹配成功或者到尾节点
for (Node h = head, p = h; p != null;) { // find & match first node
boolean isData = p.isData;
Object item = p.item;
// 如果不匹配
if (item != p && (item != null) == isData) { // unmatched
// 如果两者模式一样,则不能匹配,跳出循环后尝试入队
if (isData == haveData) // can't match
break;
// 把p的内容设置为e(如果是取元素则e是null,如果是存元素则e是元素值)
// 如果匹配成功
if (p.casItem(item, e)) { // match
// 由于控制多线程同时存取元素时出现竞争的情况
for (Node q = p; q != h;) {
Node n = q.next; // update by 2 unless singleton
// 如果head还没变,就把它更新成新的节点,成功后把它删除
if (head == h && casHead(h, n == null ? q : n)) {
h.forgetNext();
break;
} // advance and retry
// 如果新的头节点为空,或者其next为空,或者其next不匹配,跳出循环重试
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
// 唤醒p中等待的线程
LockSupport.unpark(p.waiter);
// 返回匹配到的元素
return LinkedTransferQueue.<E>cast(item);
}
}
// 如果匹配失败了,则向后推进
Node n = p.next;
// 如果p的next指向p本身,说明p节点已经有其他线程处理过了,只能从head重新开始
p = (p != n) ? n : (h = head); // Use head if p offlist
}
// 到这里说明队列中存储的节点模式与入参相同,或者到达链表尾
// 如果模式不为NOW
if (how != NOW) { // No matches available
// 新建s节点
if (s == null)
s = new Node(e, haveData);
// 尝试入队
Node pred = tryAppend(s, haveData);
// 入队失败,重试
if (pred == null)
continue retry; // lost race vs opposite mode
// 如果模式不为ASYNC,就等待被匹配
if (how != ASYNC)
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e; // not waiting
}
}
作为核心逻辑处理方法,代码还是比较复杂的,其主要执行流程是:
●先查看队列的头节点,是否与传入元素的模式一致。
●如果模式不一样,就尝试让他们匹配。如果头节点被别的线程先匹配走了,就尝试与头节点的下一个节点匹配,如此一直往后,直到匹配成功或到达链表尾为止。
●如果模式一样,或者到达链表尾,就尝试入队。
●入队的时候有可能链表尾已经被修改了,那就尾指针后移,再重新尝试入队,依此往复。
●如果入队成功了,就自旋或阻塞。如果阻塞了就等待被其它线程匹配到并唤醒。
●被唤醒之后进入下一次循环就匹配到元素了,返回匹配到的元素。
●是否需要入队及阻塞,根据操作参数不同,有四种情况:
ⅰ、tryAppend()
private Node tryAppend(Node s, boolean haveData) {
// 从尾节点开始遍历
for (Node t = tail, p = t;;) { // move p to last node and append
Node n, u; // temps for reads of next & tail
// 如果首尾都是null(链表中还没有元素)
if (p == null && (p = head) == null) {
// 就让首节点指向s
if (casHead(null, s))
return s; // initialize
}
// 如果p无法处理,则返回null(p和s节点的类型不同,不允许s入队)
else if (p.cannotPrecede(haveData))
return null; // lost race vs opposite mode
// 如果p的next不为空(不是最后一个节点),则让p重新指向最后一个节点
else if ((n = p.next) != null) // not last; keep traversing
p = p != t && t != (u = tail) ? (t = u) : // stale tail
(p != n) ? n : null; // restart if off list
// 如果CAS更新s为p的next失败(有其它线程先一步更新到p的next了),让p指向p的next,重新尝试让s入队
else if (!p.casNext(null, s))
p = p.next; // re-read on CAS failure
// 到这里说明s成功入队了
else {
// 如果p不等于t,就更新尾节点
if (p != t) { // update if slack now >= 2
while ((tail != t || !casTail(t, s)) &&
(t = tail) != null &&
(s = t.next) != null && // advance and retry
(s = s.next) != null && s != t);
}
// 返回p,即s的前一个元素
return p;
}
}
}
ⅱ、awaitMatch()
private E awaitMatch(Node s, Node pred, E e, boolean timed, long nanos) {
// 获取到期时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
// 自旋次数
int spins = -1; // initialized after first item and cancel checks
// 随机数,随机让一些自旋的线程让出CPU
ThreadLocalRandom randomYields = null; // bound if needed
for (;;) {
Object item = s.item;
// 如果s元素的值不等于e(匹配成功)
if (item != e) { // matched
// assert item != s;
// 把s的item更新为s本身,并把s中的waiter置为空
s.forgetContents(); // avoid garbage
// 返回匹配到的元素
return LinkedTransferQueue.<E>cast(item);
}
// 如果当前线程中断了或者有超时的到期了,就更新s的元素值指向s本身
if ((w.isInterrupted() || (timed && nanos <= 0)) &&
s.casItem(e, s)) { // cancel
// 尝试解除s与其前一个节点的关系(删除s节点)
unsplice(pred, s);
// 返回元素的值本身,说明没匹配到
return e;
}
// 如果自旋次数小于0,就计算自旋次数
if (spins < 0) { // establish spins at/near front
// 如果前面有节点未被匹配就返回0,如果前面有节点且正在匹配中就返回一定的次数,自旋等待
if ((spins = spinsFor(pred, s.isData)) > 0)
// 初始化随机数
randomYields = ThreadLocalRandom.current();
}
else if (spins > 0) { // spin
// 自旋次数自减
--spins;
// 随机让出CPU
if (randomYields.nextInt(CHAINED_SPINS) == 0)
Thread.yield(); // occasionally yield
}
else if (s.waiter == null) {
// 更新s的waiter为当前线程
s.waiter = w; // request unpark then recheck
}
// 如果有超时,计算超时时间,并阻塞一定时间
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos > 0L)
LockSupport.parkNanos(this, nanos);
}
// 如果没有超时,直接阻塞,等待被唤醒
else {
LockSupport.park(this);
}
}
}
④总结
●LinkedTransferQueue可以看作LinkedBlockingQueue、SynchronousQueue(公平模式)、ConcurrentLinkedQueue三者的集合体。
●LinkedTransferQueue使用双重队列的数据结构实现。
●不论存取元素都会入队。
●先尝试跟头节点比较,如果二者模式不一样,就匹配它们,匹配成功返回对方的值。
●如果二者模式一样,就入队,并自旋或阻塞等待被唤醒。
●有四种操作模式:NOW、ASYNC、SYNC、TIMED。
●LinkedTransferQueue全程都没有使用synchronized、ReentrantLock等比较重的锁,基本是通过 自旋+CAS 实现。
●入队之后,先自旋一定次数后再调用LockSupport.park()或LockSupport.parkNanos阻塞。
七、:LinkedBlockingDeque
①LinkedBlockingDeque简介
大多数BlockingQueue都是单向的FIFO队列,而LinkedBlockingDeque则是一个由链表组成的双向阻塞队列,双向队列就意味着可以从对头、对尾两端插入和移除元素,同样意味着LinkedBlockingDeque支持FIFO、FILO两种操作方式。LinkedBlockingDeque是可自定义容量的,在初始化时可以设置容量防止其过度膨胀,如果不设置,默认容量大小为Integer.MAX_VALUE。
②LinkedBlockingDeque的关键属性及类
1、关键属性
// 头节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// 表中节点个数
private transient int count;
// 容量(创建时指定)
private final int capacity;
// 锁
final ReentrantLock lock = new ReentrantLock();
// 非空条件
private final Condition notEmpty = lock.newCondition();
// 非满条件
private final Condition notFull = lock.newCondition();
通过观察属性,我们可以看到几个重要信息:
●利用ReentrantLock来保证并发安全。
●利用Condition作为多线程中消息通知机制。(当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当存入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中)
2、关键类
static final class Node<E> {
// 节点中的元素
E item;
// 前驱节点
Node<E> prev;
// 后继节点
Node<E> next;
}
③LinkedBlockingDeque的重点方法分析
1、构造方法
public LinkedBlockingDeque() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingDeque(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
}
public LinkedBlockingDeque(Collection<? extends E> c) {
this(Integer.MAX_VALUE);
final ReentrantLock lock = this.lock;
lock.lock(); // Never contended, but necessary for visibility
try {
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (!linkLast(new Node<E>(e)))
throw new IllegalStateException("Deque full");
}
} finally {
lock.unlock();
}
}
从构造方法可以直观看到,默认容量为Integer.MAX_VALUE。
2、存数据方法
ⅰ、putFirst()
public void putFirst(E e) throws InterruptedException {
// 存入元素不可为空
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 如果插入失败,使用notFull等待
while (!linkFirst(node))
notFull.await();
} finally {
// 解锁
lock.unlock();
}
}
private boolean linkFirst(Node<E> node) {
// assert lock.isHeldByCurrentThread();
// 判断是否超出容量
if (count >= capacity)
return false;
Node<E> f = first;
// 新节点的next指向头节点
node.next = f;
// 设置node为新的头节点
first = node;
// 如果没有尾节点,设置node为尾节点
if (last == null)
last = node;
// 如果有尾节点,那就将之前头节点的前驱节点设置为node
else
f.prev = node;
++count;
// 唤醒notEmpty
notEmpty.signal();
return true;
}
代码逻辑比较简单,就是将指定的元素插入双端队列的头,如果失败(空间占满)将一直等待可用空间。
ⅱ、putLast()
public void putLast(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
while (!linkLast(node))
notFull.await();
} finally {
lock.unlock();
}
}
private boolean linkLast(Node<E> node) {
// assert lock.isHeldByCurrentThread();
if (count >= capacity)
return false;
Node<E> l = last;
node.prev = l;
last = node;
if (first == null)
first = node;
else
l.next = node;
++count;
notEmpty.signal();
return true;
}
代码逻辑与上面类似,只是插入到双端队列的尾。
3、取数据方法
ⅰ、pollFirst()
public E pollFirst() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
return unlinkFirst();
} finally {
// 解锁
lock.unlock();
}
}
private E unlinkFirst() {
// assert lock.isHeldByCurrentThread();
Node<E> f = first;
// 如果头节点为空,直接返回null
if (f == null)
return null;
Node<E> n = f.next;
E item = f.item;
// 移除头节点
f.item = null;
f.next = f; // help GC
first = n;
// 移除后为空队列,尾节点设为null
if (n == null)
last = null;
// 移除后不为空队列,n的前驱节点设为null
else
n.prev = null;
--count;
notFull.signal();
return item;
}
代码逻辑比较简单,就是获取并移除此双端队列的头节点。如果双端队列为空,返回null。
ⅱ、pollLast()
public E pollLast() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return unlinkLast();
} finally {
lock.unlock();
}
}
private E unlinkLast() {
// assert lock.isHeldByCurrentThread();
Node<E> l = last;
if (l == null)
return null;
Node<E> p = l.prev;
E item = l.item;
l.item = null;
l.prev = l; // help GC
last = p;
if (p == null)
first = null;
else
p.next = null;
--count;
notFull.signal();
return item;
}
代码逻辑与上面类似,只是获取并移除的是双端队列的尾节点。
④总结
●LinkedBlockingDeque基于链表,使用双端队列思想实现。
●LinkedBlockingDeque是有界队列,不传入容量时默认为Integer.MAX_VALUE。
八、:LinkedBlockingQueue
①LinkedBlockingQueue简介
LinkedBlockingQueue是用链表实现的有界阻塞队列,队列默认大小为Integer.MAX_VALUE。
②LinkedBlockingQueue的关键属性及类
1、关键属性
// 容量(创建时指定)
private final int capacity;
// 表中节点个数
private final AtomicInteger count = new AtomicInteger();
// 头节点
transient Node<E> head;
// 尾节点
private transient Node<E> last;
// 取锁
private final ReentrantLock takeLock = new ReentrantLock();
// 非空条件
private final Condition notEmpty = takeLock.newCondition();
// 存锁
private final ReentrantLock putLock = new ReentrantLock();
// 非满条件
private final Condition notFull = putLock.newCondition();
通过观察属性,我们可以看到几个重要信息:
●利用ReentrantLock来保证并发安全。存取使用了不同的锁,锁分离提高了效率。
●利用Condition作为多线程中消息通知机制。(当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当存入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中)
2、关键类
static class Node<E> {
// 节点中的元素
E item;
// 后继节点
Node<E> next;
}
③LinkedBlockingQueue的重点方法分析
1、构造方法
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
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) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);
} finally {
putLock.unlock();
}
}
从构造方法可以直观看到,默认容量为Integer.MAX_VALUE。
2、存数据方法
public void put(E e) throws InterruptedException {
// 存入元素不可为空
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 加存锁
putLock.lockInterruptibly();
try {
// 如果队列满了,使用notFull等待
while (count.get() == capacity) {
notFull.await();
}
// 如果队列不满,执行入队
enqueue(node);
c = count.getAndIncrement();
// 如果当前队列长度小于容量,唤醒notFull(因为使用锁分离而多出的操作)
if (c + 1 < capacity)
notFull.signal();
} finally {
// 解存锁
putLock.unlock();
}
// 如果原队列长度为0,现在存入了一个元素,唤醒notEmpty
if (c == 0)
signalNotEmpty();
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
// 将元素加在尾节点后面
last = last.next = node;
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
// 加取锁
takeLock.lock();
try {
// 唤醒notEmpty
notEmpty.signal();
} finally {
// 解取锁
takeLock.unlock();
}
}
从代码可以看出,存入队列的数据不能为空。整体逻辑比较简单:
●加存锁。
●如果队列满了,使用notFull等待。否则入队。
●如果入队后元素数量小于容量,唤醒其它阻塞在notFull上的线程。
●解存锁。
●如果存入元素前队列中元素数量为0,唤醒notEmpty。
3、取数据方法
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
if (c > 1)
notEmpty.signal();
} finally {
// 解取锁
takeLock.unlock();
}
// 如果取之前队列长度等于容量,现在取出了一个元素,唤醒notFull
if (c == capacity)
signalNotFull();
return x;
}
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
// 头节点本身不存储任何元素,把头节点后继节点清空设置为头节点,返回其原来内容
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;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
// 加存锁
putLock.lock();
try {
// 唤醒notFull
notFull.signal();
} finally {
// 解存锁
putLock.unlock();
}
}
代码整体逻辑比较简单:
●加取锁。
●如果队列无元素,使用notEmpty等待。否则出队。
●如果出队前元素数量大于1,唤醒其它阻塞在notEmpty上的线程。
●解取锁。
●如果取出元素之前队列长度等于容量,唤醒notFull。
④总结
●LinkedBlockingQueue采用单链表的形式实现。
●LinkedBlockingQueue采用两把锁的锁分离技术实现入队出队互不阻塞。
●LinkedBlockingQueue是有界队列,不传入容量时默认为Integer.MAX_VALUE。
系列文章传送门:
JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池