文章目录
在并发编程中,有时候需要使用线程安全的队列。如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁) 等方式来实现。非阻塞的实现方式则可以使用循环CAS的方式来实现。
下面分析的是阻塞队列。
1.什么是阻塞队列
阻塞队列是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除方法。
-
支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
-
支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素,消费者则从队列里取元素。
2.队列Queue接口核心方法
先简单介绍下JAVA容器中的队列有哪些接口及使用方法:
3.阻塞队列BlockigQueue接口核心方法
阻塞队列,本质上来说还是属于队列,也就是说阻塞队列继承了队列的功能。
4.常用的阻塞队列
-
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
-
LinkedBlockingQueue:由链表结构组成的有界阻塞队列
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
-
DelayQueue:使用优先级队列实现的无界阻塞队列
-
SynchronousQueue:不存储元素的阻塞队列
-
LinkedTransferQueue:由链表结构组成的无界阻塞队列
-
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
以下逐一介绍这7种阻塞队列
4.1ArrayBlockingQueue(有界阻塞数组队列)
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下采用非公平锁的方式实现,可以通过构造器传参控制是采用公平锁还是非公平锁实现。先看看ArrayBlockingQueue类图关系:
4.1.1阻塞队列的使用
模拟生产者与消费者
private static void consumeDemo() throws InterruptedException {
Thread consume = new Thread(() -> {
while (true) {
try {
Integer a = consumeQueue.take();
System.out.println("消费:" + a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread product = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
consumeQueue.put(i);
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
consume.start();
TimeUnit.SECONDS.sleep(5);
product.start();
}
4.1.2初始化队列
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 初始化Object数组
this.items = new Object[capacity];
// 初始化锁(可以指定公平与非公平)
lock = new ReentrantLock(fair);
// 非空condition队列,队列空时用于阻塞获取元素的线程
notEmpty = lock.newCondition();
// 非满condition队列,队列满时用于阻塞添加元素的线程
notFull = lock.newCondition();
}
4.1.3添加元素
add、offer、put三个方法都是添加元素,但是各自的具体实现又不一样,各自的使用场景也不相同。这三个方法在添加元素的时候都会先获得Lock锁
,添加成功后释放锁
。这里主要介绍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)
// 重新从0开始计数,前面队列满了会被阻塞,而至少移除一个元素之后才会唤醒添加元素。
// 而移除元素也是从头开始移除的,所以不会覆盖没有消费的元素
putIndex = 0;
// 元素个数+1
count++;
// 唤醒获取元素的线程
// 获取元素时,有可能队列为空。会阻塞再次等待
notEmpty.signal();
}
4.1.4获取元素
remove、poll、take三个方法都是获取元素,但是各自的具体实现又不一样,各自的使用场景也不相同。这三个方法在获取元素的时候都会先获得Lock锁
,获取成功后释放锁
。这里主要介绍take
方法
peek与element方法与上面的方法又不相同,只会读取元素而不会把他们从队列中移除。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁,可异常中断
lock.lockInterruptibly();
try {
// 队列为空
while (count == 0)
// 阻塞当前线程获取元素
// 当队列中添加元素时,会唤醒此线程继续去获取队列中的元素
notEmpty.await();
// 队列不为空,获取元素
return dequeue();
} finally {
// 释放锁
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
// takeIndex维护当前移除元素的位置
E x = (E) items[takeIndex];
// 移除元素所在的位置设置为null
items[takeIndex] = null;
//
if (++takeIndex == items.length)
// 如果移除元素已经到队列尾部
// 从0开始去获取元素
takeIndex = 0;
// 队列中的元素个数 - 1
count--;
// 针对使用迭代器得遍历
if (itrs != null)
itrs.elementDequeued();
// 移除元素之后唤醒添加元素的线程
notFull.signal();
return x;
}
使用take方法去阻塞获取元素,当阻塞时使用add方法也会继续take读取,只要是添加元素就会signal
4.2 LinkedBlockingQueue(有界阻塞链表队列)
由链表结构组成的有界阻塞队列,遵循先进先出(FIFO)的原则,和ArrayBlockingQueue的区别是ArrayBlockingQueue内部维护的是一个数组,通过数组下标来维护队列,而LinkedBlockingQueue维护的是一个链表,通过Node来维护队列。同时有两把锁(入队和出队用不同的锁),原子个数count是使用AtomicInteger修饰。
4.2.1 初始化队列
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
public LinkedBlockingQueue(int capacity) {
// 容量<=0,直接异常
if (capacity <= 0) throw new IllegalArgumentException();
// 初始化容量
this.capacity = capacity;
// 初始化一个队列,head与last节点为相同的node节点
last = head = new Node<E>(null);
}
// Node节点,含有下一个节点的引用
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
4.2.2 添加元素
public void put(E e) throws InterruptedException {
// 元素为空,异常
if (e == null) throw new NullPointerException();
int c = -1;
// 初始化Node
Node<E> node = new Node<E>(e);
// 获得put锁
final ReentrantLock putLock = this.putLock;
// 队列的大小,AtomicInteger原子类,并发安全的。
final AtomicInteger count = this.count;
// 加锁
putLock.lockInterruptibly();
try {
// 队列满了
while (count.get() == capacity) {
// 阻塞其他线程继续添加元素
notFull.await();
}
// 队列没有满,将Node加入队列
enqueue(node);
// 获取count的值,并同时+1
c = count.getAndIncrement();
// 如果队列没有满(count还是原队列的大小)c + 1因为此线程put元素加入队列成功。
if (c + 1 < capacity)
// 唤醒 其他阻塞添加元素 的线程
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
// 说明在添加元素之前,队列是空的。有可能其他线程在获取元素,此时正在被阻塞。这里需要唤醒
if (c == 0)
// 唤醒获取元素的线程
signalNotEmpty();
}
private void enqueue(Node<E> node) {
// 将添加的元素封装为node加入队列中。head指向空信息节点,不是具体添加的元素 ,但是它拥有添加元素的next引用
last = last.next = node;
}
private void signalNotEmpty() {
// 唤醒获取元素的线程
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
4.3.3获取元素
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();
// 先获得移除元素之前队列的大小,之后把值更新为 - 1
c = count.getAndDecrement();
// 队列中还存在元素,可以唤醒其他阻塞获取元素的线程去获得元素
if (c > 1)
notEmpty.signal();
} finally {
// 释放锁
takeLock.unlock();
}
// 队列满时,移除一个后需要唤醒加入元素的线层去添加元素
// c == capacity 为移除元素之前的容量,移除后,队列肯定不是满的
if (c == capacity)
signalNotFull();
return x;
}
// 把head节点的next节点设置为head,并返回其value值后把item设置为null
private E dequeue() {
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.signal();
} finally {
putLock.unlock();
}
}
4.3LinkedBlockingDeque(有界阻塞双向链表队列)
LinkedBlockingDeque和LinkedBlockingQeque一样是由链表结构组成,但是LinkedBlockingDeque是个双向链表,比LinkedBlockingQeque多一个pre节点的引用。
双向队列因为多了一个操作队列的入口,所以相比较于LinkedBlockingQeque单向队列中多了addFirst、 addLast、offerFirst、offerLast、peekFirst、peekLast、putFirst、putLast、takeFirst、takeLast等方法。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst,而take方法却等同于takeFirst,使用时需要注意。
4.3.1初始化队列
transient Node<E> first;
transient Node<E> last;
/** Number of items in the deque */
private transient int count;
/** Maximum number of items in the deque */
private final int capacity;
/** Main lock guarding all access */
final ReentrantLock lock = new ReentrantLock();
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();
// 初始化的时候没有设置任何节点,仅仅只是设置了一个容量
public LinkedBlockingDeque(int capacity) {
// 初始化容量<=0,抛异常
if (capacity <= 0) throw new IllegalArgumentException();
// 设置队列的容量
this.capacity = capacity;
}
static final class Node<E> {
E item;
// prev节点
Node<E> prev;
// next节点
Node<E> next;
Node(E x) {
item = x;
}
}
4.3.2添加元素
4.3.2.1 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 {
// 队列满了,阻塞当前线程添加元素
while (!linkFirst(node))
notFull.await();
} finally {
lock.unlock();
}
}
// 把当前节点,加入链表的首个位置
private boolean linkFirst(Node<E> node) {
if (count >= capacity)
return false;
Node<E> f = first;
node.next = f;
first = node;
if (last == null)
last = node;
else
f.prev = node;
++count;
notEmpty.signal();
return true;
}
4.3.2.1 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;
}
4.3.3获取元素
4.3.3.1takeFirst
public E takeFirst() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkFirst()) == null)
notEmpty.await();
return x;
} finally {
lock.unlock();
}
}
private E unlinkFirst() {
// assert lock.isHeldByCurrentThread();
Node<E> f = first;
if (f == null)
return null;
Node<E> n = f.next;
E item = f.item;
f.item = null;
f.next = f; // help GC
first = n;
if (n == null)
last = null;
else
n.prev = null;
--count;
notFull.signal();
return item;
}
4.3.3.2takeLast
public E takeLast() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E x;
while ( (x = unlinkLast()) == null)
notEmpty.await();
return x;
} 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;
}
4.4PriorityBlockingQueue(无界阻塞优先队列)
PriorityBlockingQueue是一个支持优先级的无界阻塞队列(大小受限于内存)。和前面介绍的三种有界队列相比,无界队列的最大区别是即使初始化的时候指定了长度,那么当队列元素达到上限后队列也会自动进行扩容,所以PriorityBlockingQueue在添加元素的时候不会发生阻塞,而如果扩容后的大小超过了内存限制,会抛出OutOfMemoryError错误。
学习PriorityBlockingQueue之前,我们需要先学习下二叉堆相关知识。可参考下面博客:
数据结构:二叉堆_wangbo199308的博客-CSDN博客
二叉堆用数组来实现。
- 节点的左子节点是 2*index+1
- 节点的右子节点是 2*index+2
- 节点的父节点是 (index-1)/2
- 叶子节点的下标为index[n/2]到index[n-1]。如一个长度为13的数组中,index[6]到index[12]位置的元素均属于叶子节点。
默认情况下PriorityBlockingQueue队列元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者在初始化时,可以指定构造参数Comparator来对元素进行排序。
注意:PriorityBlockingQueue不能保证相同优先级元素的顺序(即两个值排序一样时,不保证顺序)。类图如下:
可以看到提供了4个 构造器:
- PriorityBlockingQueue():
初始化一个默认大小(11)长度的队列,并使用默认自然排序。 - PriorityBlockingQueue(int):
初始化一个指定大小的长度的队列,并使用默认自然排序。 - PriorityBlockingQueue(int,Comparator):
初始化一个指定大小的队列,并按照指定比较器进行排序。 - PriorityBlockingQueue(Collection):
根据传入的集合进行初始化并堆化,如果当前集合是SortedSet或者PriorityBlockingQueue类型,则保持原有顺序,否则使用自然排序进行堆化。
默认情况下元素采用自然顺序升序排序,可以通过指定Comparator来对元素进行排序
。
4.4.1初始化
private transient Object[] queue;
/**
* The number of elements in the priority queue.
*/
private transient int size;
/**
* The comparator, or null if priority queue uses elements'
* natural ordering.
*/
private transient Comparator<? super E> comparator;
/**
* Lock used for all public operations
*/
private final ReentrantLock lock;
/**
* 出队,队列为空条件队列
*/
private final Condition notEmpty;
/**
* 扩容标记
*/
private transient volatile int allocationSpinLock;
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];
}
我们看到只有一个Condition队列,这个是用来阻塞出队线程的,入队线程不会被阻塞。
接下来我们主要看看第4个构造器,是如何初始化一个队列的:
public PriorityBlockingQueue(Collection<? extends E> c) {
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
// true表示需要堆化即需要重排序
boolean heapify = true; // true if not known to be in heap order
// true表示需要筛选空值
boolean screen = true; // true if must screen for nulls
// 如果集合是SortedSet类型则不需要堆化
if (c instanceof SortedSet<?>) {
SortedSet<? extends E> ss = (SortedSet<? extends E>) c;
this.comparator = (Comparator<? super E>) ss.comparator();
heapify = false;
}
// 如果集合是PriorityBlockingQueue类型则不需要筛选空值
else if (c instanceof PriorityBlockingQueue<?>) {
PriorityBlockingQueue<? extends E> pq =
(PriorityBlockingQueue<? extends E>) c;
this.comparator = (Comparator<? super E>) pq.comparator();
screen = false;
// 如果pq就是一个PriorityBlockingQueue则不需要堆化
if (pq.getClass() == PriorityBlockingQueue.class) // exact match
heapify = false;
}
// 把集合转为
Object[] a = c.toArray();
// 集合的长度
int n = a.length;
// //如果c.torray()失败,重新复制一个数组
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();
}
private void heapify() {
// 二叉堆数据
Object[] array = queue;
int n = size;
// 叶子节点为在数组中的下标为(n/2)到(n-1)
// 非叶子节点的最后元素下标为(n/2)-1
// 初始化时,叶子节点为子节点,不需要进行下沉操作
int half = (n >>> 1) - 1;
// 比较器
Comparator<? super E> cmp = comparator;
if (cmp == null) {
for (int i = half; i >= 0; i--)
// 默认比较器下沉
siftDownComparable(i, (E) array[i], array, n);
}
else {
for (int i = half; i >= 0; i--)
// 指定比较器下沉
siftDownUsingComparator(i, (E) array[i], array, n, cmp);
}
}
下面初始化一个数组queue [8,5,2,7,6,4,1,9,3],初始化为二叉堆的一个步骤。二叉堆的初始化会从最后一个非叶子节点开始,在数组中的下标为(n >>> 1) - 1。
从queue[3]=7这个元素向前循环下沉操作。需要下沉四次
这里介绍默认比较器下沉,siftDownUsingComparator与siftDownComparable几乎差不多。
// k:下沉元素的下标
// x:下沉元素的值
// array:二叉堆数组
// n:二叉堆数组元素的个数
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
// 元素个数大于0
// 入队的第一个元素不做任何操作,就是二叉堆的根
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
// 第一个叶子节点的下标
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
// 当前节点K的左子节点2K+1
int child = (k << 1) + 1; // assume left child is least
// 左子节点的值
Object c = array[child];
// 当前节点K的右子节点2K+2
int right = child + 1;
// 如果左子节点大于右子节点
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
// 先将两个子节点替换(只是临时变量替换,实际不会替换)
// 找出compare后的最小子节点
c = array[child = right];
// 如果当前节点小于替换后的左子节点,跳出循环
if (key.compareTo((T) c) <= 0)
break;
// 当前节点大于替换后的左子节点,则将替换后的左子节点赋值给父节点
array[k] = c;
// 子节点最小的元素下标。循环判断子节点也需要下沉操作
k = child;
}
// 将父节点赋值给子节点
array[k] = key;
}
}
上面的主要逻辑为:
- 将当前循环节点的左右子节点比较,确保拿到最小子节点的下标child
- 再将child对应的元素和父节点比较,确保父节点<最小子节点
- 最后会再次确认当前元素与最小子节点(可能是左也可能是右)的子节点(如果有的话)进行大小比较,依此类推,完成元素下沉。
排序后为:
4.4.2添加元素
入队,首先检查有没有达到数组容量的上限。达到后进行扩容。之后把元素添加到数据最后进行上浮。
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.signal();
} finally {
// 释放锁
lock.unlock();
}
// 添加成功,返回true
return true;
}
4.4.2.1上浮
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];
// 当前入队节点大于父节点,break退出,不做任何操作
if (key.compareTo((T) e) >= 0)
break;
// 当前入队节点小于父节点,将父节点替换到末尾。
array[k] = e;
// 继续把当前节点的父节点进行上浮比较
k = parent;
}
array[k] = key;
}
4.4.2.2扩容
private void tryGrow(Object[] array, int oldCap) {
// 扩容前先释放锁(扩容可能会费时,先让出锁,让出队线程可以正常操作)
lock.unlock(); // must release and then re-acquire main lock
Object[] newArray = null;
// CAS操作,只有一个线程可以扩容
if (allocationSpinLock == 0 &&
UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
0, 1)) {
try {
// 原始容量小于64时扩容增加oldCap + 2
// 原始容量大于等于64时扩容增加oldCap/2
int newCap = oldCap + ((oldCap < 64) ?
(oldCap + 2) : // grow faster if small
(oldCap >> 1));
// 大于当前最大容量则可能溢出
if (newCap - MAX_ARRAY_SIZE > 0) { // possible overflow
// 扩大一个元素超出Integer最大值或超过最大容量则抛出异常
int minCap = oldCap + 1;
if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
throw new OutOfMemoryError();
// 容量初始化为MAX_ARRAY_SIZE
newCap = MAX_ARRAY_SIZE;
}
// 根据扩容后容量初始化一个新数组
if (newCap > oldCap && queue == array)
newArray = new Object[newCap];
} finally {
// 扩容标识重置为0,可以进行下次扩容
allocationSpinLock = 0;
}
}
// 扩容新数组,前面CAS失败,有线程在扩容,让出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);
}
}
4.4.3获取元素
- 先拿到第一个元素(需要返回)和最后一个元素
- 然后将最后一个元素置为空
- 用存好的最后一个元素的值从头开始下沉
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;
}
private E dequeue() {
// 元素个数-1
int n = size - 1;
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;
}
}
4.5DelayQueue(无界阻塞延迟队列)
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue
来实现。队列中的元素必须实现Delayed
接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
DelayQueue非常有用, 可以将DelayQueue运用在以下应用场景。
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期, 使用一个线程循环查询DelayQueue, 一旦能从DelayQueue中获取元素时, 表示缓存有效期到了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间, 一旦从DelayQueue中获取到任务就开始执行, 比如TimerQueue就是使用DelayQueue实现的
Demo:延迟获取元素,每个元素延迟的时间不同。
public class DelayQueueDemo {
private static DelayQueue<DelayedInteger> delayQueue = new DelayQueue<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 5; i++) {
// 入队5个元素,每个延迟时间不同
DelayedInteger myElement = new DelayedInteger(i, i * 10000);
delayQueue.put(myElement);
}
while (true) {
DelayedInteger element = delayQueue.take();
System.out.println(element.getElement());
if (delayQueue.size() == 0) {
break;
}
}
}
static class DelayedInteger implements Delayed {
private int element;
private long expireTime;
public DelayedInteger() {
}
public DelayedInteger(int element, long expireTime) {
this.element = element;
this.expireTime = System.currentTimeMillis() + expireTime;
}
public int getElement() {
return element;
}
public void setElement(int element) {
this.element = element;
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
// 该方法返回当前元素还需要延迟多长时间,单位是纳秒
@Override
public long getDelay(TimeUnit unit) {
//类里面接收的是毫秒,但是getDelay方法在DelayQueue里面传的是纳秒,所以这里需要进行一次单位转换
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
// 这里的排序要确定最先到期的放在第一位,否则会阻塞住后面已经到期的
// 当先到期的被未到期的阻塞,未到期的出队后之前先到期的不用再延迟
return Long.valueOf(expireTime).compareTo(Long.valueOf(((DelayedInteger) o).getExpireTime()));
}
}
}
类图:
4.5.1初始化
DelayQueue队列的元素是存在其内部维护的PriorityQueue。
// 锁,保证线程安全
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();
public DelayQueue() {}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
4.5.2添加元素
无界阻塞队列,添加元素不会阻塞。添加元素是把元素加入优先队列priorityQueue中
public void put(E e) {
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();
}
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
// 扩容
if (i >= queue.length)
grow(i + 1);
// 元素个数+1
size = i + 1;
// 入队第一个元素,放在数组第一个位置
if (i == 0)
queue[0] = e;
else
// 不是优先队列的第一个元素,需要执行上浮操作
siftUp(i, e);
return true;
}
4.5.3获取元素
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);
// 如果到期了,则调用poll方法取元素并直接返回
if (delay <= 0)
return q.poll();
first = null;
// 有头结点线程正在等待到期时间,所以直接阻塞
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
// 设置头结点线程
leader = thisThread;
try {
// 阻塞指定时间
available.awaitNanos(delay);
} finally {
// 设置头结点线程为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 头结点为空,并且队列中还有元素。
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
4.5.4Leader-Follower线程模型
在Leader-follower线程模型中每个线程有三种模式:
- leader:只有一个线程成为leader,如DelayQueue如果有一个线程在等待元素到期,则其他线程就会阻塞等待
- follower:会一直尝试争抢leader,抢到leader之后才开始干活
- processing:处理中的线程
DelayQueue队列中有一个leader属性:private Thread leader = null;
用到的就是Leader-Follower线程模型。
当有一个线程持有锁,设置了leader属性,正在等待元素到期时,则成为了leader,其他线程就直接阻塞。
出队流程图如下:
4.6SynchronousQueue(不存储元素的阻塞队列)
SynchronousQueue是一个不存储元素的阻塞队列,每一个入队操作必须等待一个出队操作,否则不能继续添加元素。其没有任何内部容量。
- put(E)操作时,队列中的节点代表一个元素,也就说表示的是数据
- take()操作时,如果队列中无元素,会放一个null的项到队列中占位,这时候表示的是一个请求,而不是一个数据。
public E peek() {
return null;
}
/**
* Returns an empty iterator in which {@code hasNext} always returns
* {@code false}.
*
* @return an empty iterator
*/
public Iterator<E> iterator() {
return Collections.emptyIterator();
}
public boolean isEmpty() {
return true;
}
/**
* Always returns zero.
* A {@code SynchronousQueue} has no internal capacity.
*
* @return zero
*/
public int size() {
return 0;
}
不能执行peek()、element()之类的方法获取一个元素,因为一个元素只有在尝试移除的时候(remove、poll)才会出现,也不能使用任何方法去插入一个元素,除非另一个线程正好在尝试移除它(take),也不能去迭代,因为没有任何东西可以迭代。
- 对于add、offer两个不阻塞入队操作,必须有出队线程take操作,才能成功添加当前元素。不然add抛出异常,offer返回false
- 对于put阻塞入队操作,必须有出队线程的出队,才能成功添加元素。
- 对于take阻塞出队操作,必须有入队线程的入队操作,才能成功移除元素。
SynchronousQueue的head是第一个排队插入线程试图添加到队列的元素,如果没有这样的排队线程那么就没有元素可以被移除,所以执行poll()的时候就会返回null。
SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。
SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue,因为其内部是通过CAS和自旋来实现并发,没有通过锁来控制,减少了锁的开销。
4.6.1初始化
SynchronousQueue提供了两个构造器,默认构造器是非公平策略,可以通过第二个构造器传入参数true来构造一个公平策略
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
// TransferQueue的构造方法
TransferQueue() {
QNode h = new QNode(null, false); // initialize to dummy node.
head = h;
tail = h;
}
TransferQueue内部是通过QNode节点来维持一个队列,QNode是TransferQueue的一个内部类:
static final class QNode {
// 下一个节点
volatile QNode next; // next node in queue
// 节点中的值,通过CAS操作。将空变为非空(put),将非空变为空(take)
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // to control park/unpark
// false表示take,true表示put
final boolean isData;
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
// ...
private static final sun.misc.Unsafe UNSAFE;
private static final long itemOffset;
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = QNode.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
4.6.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();
}
}
4.6.2.1.transfer
// timed:false表示put,true表示take
E transfer(E e, boolean timed, long nanos) {
QNode s = null;
// e==null表示当前是消费者(take操作),e!=null表示当前是生产者(put操作)
boolean isData = (e != null);
for (;;) {
// 头结点(初始化时item为空的节点)
QNode t = tail;
// 尾节点
QNode h = head;
// 表示还没有初始化(初始化之后不可能为空),继续自旋
if (t == null || h == null)
continue;
// head=tail或者tail节点的模式和当前操作模式相同
if (h == t || t.isData == isData) {
// 获得当前tail节点的next节点
QNode tn = t.next;
// t和tail不一致,说明有其他线程操作过,继续自旋
if (t != tail)
continue;
// tail.next正常是null,不为null说明其他线程新增了元素到tail.next
if (tn != null) {
// 尝试帮助其他线程将tail.next设置为tail,然后继续自旋
advanceTail(t, tn);
continue;
}
// take操作。如果当前调用的是超时方法,且到时间了,直接返回null
if (timed && nanos <= 0)
return null;
// 第一次进来肯定是null。当cas next与tail两个操作失败时,自旋进入此时已经初始化过不为空
if (s == null)
// 如果是put进来,e不为null,如果是take进来,e==null,也就是初始化了一个空节点
s = new QNode(e, isData);
// CAS设置tail节点的next节点,cas失败(并发竞争失败)继续自旋
if (!t.casNext(null, s)) // failed to link in
continue;
// 将s设置为新的tail节点
// 这里是一定会成功的,因为上面的cas完成之后,这一步不执行完成,其他线程只能一直自旋等待
advanceTail(t, s);
// 节点添加进去之后,阻塞等待,直接消费者线程来消费
// 被正常take,返回为null
Object x = awaitFulfill(s, e, timed, nanos);
// x==s表示put线程已经中断
if (x == s) {
// 清除
clean(t, s);
// 直接返回null
return null;
}
if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (x != null) // and forget fields
s.item = s;
s.waiter = null;
}
return (x != null) ? (E)x : e;
} else { // complementary-mode
QNode m = h.next; // node to fulfill
if (t != tail || m == null || h != head)
continue; // inconsistent read
Object x = m.item;
if (isData == (x != null) || // m already fulfilled
x == m || // m cancelled
!m.casItem(x, e)) { // lost CAS
advanceHead(h, m); // dequeue and retry
continue;
}
advanceHead(h, m); // successfully fulfilled
LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;
}
}
}
4.6.2.2advanceTail
// cas设置尾节点
void advanceTail(QNode t, QNode nt) {
if (tail == t)
UNSAFE.compareAndSwapObject(this, tailOffset, t, nt);
}
4.6.2.3awaitFulfill
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中的item变为QNode s
s.tryCancel(e);
// 当前节点的item
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);
}
}
4.6.2.4tryCancel
void tryCancel(Object cmp) {
UNSAFE.compareAndSwapObject(this, itemOffset, cmp, this);
}
A、B、c三个线程put数据
4.6.3移除元素(已经PUT)
public E take() throws InterruptedException {
E e = transferer.transfer(null, false, 0);
if (e != null)
return e;
Thread.interrupted();
throw new InterruptedException();
}
4.6.3.1transfer
E transfer(E e, boolean timed, long nanos) {
QNode s = null;
// take数据,e为null, isData为null
// put为true,take为false
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // saw uninitialized value
continue; // spin
// take元素前已经PUT不会走此分支
if (h == t || t.isData == isData) { // empty or same-mode
// ....
} else {
// 互补模式
// 先put再take走此分支
// 获取头结点后的第一个节点
QNode m = h.next;
// 不一致情况,继续下一次自旋
if (t != tail || m == null || h != head)
continue; // inconsistent read
// 拿到节点中的item
Object x = m.item;
// 元素已经被其他元素拿走了(item变为null),继续自旋
// 说明线程put已经被中断
if (isData == (x != null) ||
x == m ||
// take操作把m节点的item变为null
!m.casItem(x, e)) {
// 上面任意一个条件失败都说明当前元素已经不存在,所以帮助那个线程cas替换一下头节点
advanceHead(h, m);
// 自旋处理下一个节点
continue;
}
// 说明元素是被当前线程取走了,cas替换一下头节点
advanceHead(h, m);
// 唤醒阻塞put元素的线程继续传递元素
LockSupport.unpark(m.waiter);
// 返回当前元素
return (x != null) ? (E)x : e;
}
}
}
之后唤醒put元素的线程,还在transfer中
4.6.4先take再put
上面已经分析了,先put元素在take元素。
下面简单介绍下先take再put。主要画出put与take操作的图,代码自行分析
先take再put与先put再take为互补模式。只不过 take返回数据的位置不一样。
4.6.5总结
主要区别如下:
-
put —>take
- put
-
将元素初始化成为一个QNode。
-
将tail.next指向当前新构建的QNode(CAS操作)。
-
将新构建的QNode设置为tail节点(CAS操作)。
-
将当前QNode节点中的waiter属性设置为当前线程(awaitFulfill方法)。
-
挂起前线程(awaitFulfill方法)。
- take
-
将head.next中的item设置为null(CAS操作)。
-
将head.next设置为新的head节点(advanceHead方法)。
-
将原head节点的next指向自己(advanceHead方法)。
-
通过原节点的waiter属性,将原先线程唤醒。
-
返回获取到的元素(take线程transfer的else分支)。
-
take—>put
- take
-
将一个null元素初始化成为一个QNode
-
将tail.next指向当前新构建的QNode(CAS操作)。
-
将新构建的QNode设置为tail节点(CAS操作)。
-
将当前QNode节点中的waiter属性设置为当前线程(awaitFulfill方法)。
-
挂起前线程(awaitFulfill方法)。
- put
-
将head.next中的item设置为1(CAS操作)。
-
将head.next设置为新的head节点(advanceHead方法)。
-
将原head节点的next指向自己(advanceHead方法)。
-
通过原节点的waiter属性,将原先线程唤醒。
-
返回成功put进去到的元素。(take线程transfer的if分支)
4.7LinkedTransferQueue(无界阻塞链表队列)
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。LinkedTransferQueue和SynchronousQueue中的公平策略使用的算法是一样的,唯一的区别是SynchronousQueue内部不会存储哪怕1个元素,而LinkedTransferQueue内部会存储元素。
TransferQueue扩展的transfer方法会一直阻塞直到添加的数据被消费者消费,这点与上一篇学的SynchronousQueue的put方法很相似,所以LinkedTransferQueue具有SynchronousQueue的功能。
松弛度
正常队列中,当移除一个元素的时候,就会同步移动head和tail节点的指针,为了最大程序的保证性能LinkedTransferQueue不会实时去更新head和tail的指针,而是引入了一个松弛度的概念。**松弛度指的是head值与第一个不匹配节点之间的目标最大距离,反之对tail也是如此。**这个值这一般为1-3(根据经验得出),在LinkedTransferQueue中松弛度定义为2。因为如果太大了会增加缓存丢失的成本或者长遍历链的风险,而较小的话就会增加CAS的开销。
相比较于其他阻塞队列,多了一个TransferQueue接口,我们先来看看TransferQueue接口中核心的几个方法:
4.7.1初始化
private static final int NOW = 0; // for untimed poll, tryTransfer
private static final int ASYNC = 1; // for offer, put, add
private static final int SYNC = 2; // for transfer, take
private static final int TIMED = 3; // for timed poll, tryTransfer
public LinkedTransferQueue() {
}
public LinkedTransferQueue(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;
}
初始化的时候什么也不做,并不会在内部构造一个初始节点。addAll()实际上也是循环调用了add(E)方法:
4.7.2xfer
通过查看源码发现put、offer、add、take、poll方法包括tryTransfer、transfer都是调用的xfer方法。所以我们重点分析xfer
方法。
-
e表示要添加的数据,take与poll为null
-
haveData表示是否有数据,添加类方法为true,消费类为false
-
how表示方法阻塞方式,LinkedTransferQueue定义了4个静态变量NOW、ASYNC、SYNC、TIMED
- NOW表示不阻塞在poll、tryTransfer方法使用:for untimed poll, tryTransfer
- ASYNC在put、offer、add方法使用:for offer, put, add
- SYNC表示阻塞用于take方法:for transfer, take
- TIMED用于poll、tryTransfer的延时方法:for timed poll, tryTransfer
-
nanos表示最大阻塞多少时间,poll和tryTransfer方法会用到
private E xfer(E e, boolean haveData, int how, long nanos) {
if (haveData && (e == null))//如果当前是put操作,且e==null,则抛出异常
throw new NullPointerException();
Node s = null; // the node to append, if needed
retry:
for (;;) { // restart on append race
//从head开始循环匹配
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中的item替换成e
if (p.casItem(item, e)) { // match
//假如有一个队列是有元素的,第一次被take()的时候,q==h是进不了for循环的,所以会直接返回
//第2次进来会先匹配一次head节点,匹配不上,在匹配第2个节点,这就相当于松弛度=2了,所以
//这时候是满足条件的,可以进入for循环,
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
if ((h = head) == null ||
(q = h.next) == null || !q.isMatched())
break; // unless slack < 2
}
LockSupport.unpark(p.waiter);
return LinkedTransferQueue.<E>cast(item);
}
}
Node n = p.next;
p = (p != n) ? n : (h = head); // Use head if p offlist
}
if (how != NOW) { // No matches available
if (s == null)
s = new Node(e, haveData);//初始化节点
Node pred = tryAppend(s, haveData);
if (pred == null)
continue retry; // lost race vs opposite mode
if (how != ASYNC)//take()或者带超时时间的方法会走这里
return awaitMatch(s, pred, e, (how == TIMED), nanos);
}
return e; // not waiting
}
}
这个方法也要分为两种方式,先put(E)再take()和先take()再put(E)。
先put(E)再take()
put(E)操作不会进行阻塞,成功之后直接返回。
线程t1过来put(1)
因为head和tail都是null(一开始不会初始化队列),所以上面的第2个for循环是进不去的,会走到后面这里初始化node,并加入到队列中,最终得到如下队列:
可以看到,这时候tail节点并没有被初始化,这是因为利用了松弛度,松弛度要等于2才会移动tail指针(这是一种性能的优化),我们看看tryAppend方法(主要是看红框部分):
主要分为以下步骤:
- 1、初始化Node节点
- 2、将Node节点设为head节点
线程t2过来put(2)
线程t2再进来put的时候,因为满足松弛度=2了,这时候就会移动tail指针,所以会得到如下队列:
主要分为以下步骤:
- 1、初始化Node节点
- 2、将Node节点设为head.next节点
- 3、达到松弛度,将新Node设置为tail节点
后面如果再有元素过来添加,到第3个元素的时候,tail也是不会移动的,要第3个元素才会移动tail,这里就不再继续举例了。
线程t3过来take()
这时候来take会将head节点的元素设为null,然后直接返回,得到如下队列:
这时候因为松弛度还没达到2,不会移动head指针。
主要经过如下步骤:
- 1、将head节点中的item设置为null。
- 2、返回获取到的item,。
线程t4过来take()
这时候首先会循环head节点,发现不匹配,然后循环到head.next,得到如下队列:
这里因为松弛度达到2,所以会移动head指针。
主要经过如下步骤:
- 1、循环head节点,发现不匹配。
- 2、循环head.next,匹配上,将head.next中item设置为null。
- 3、移动head指针,指向head.next。
先take()再put(E)
先take()的时候,因为队列中还没有元素,所以会先自旋,自旋一定次数之后就阻塞,直到有元素put(E)进来然后唤醒线程。
线程t1过来take()
和上面第一次put(E)一样,上面的第2个for循环进不去,会走到后面这里初始化node,并加入到队列中,最终得到如下队列:
主要经过如下步骤:
- 1、初始化一个item=null的Node节点。
- 2、将Node节点设置为head节点。
- 3、自旋一定次数(awaitMatch方法)。
- 4、达到自旋次数后还没有线程过来take(),执行park挂起线程(awaitMatch方法)。
注意,上面的Node中也有一个waier属性用来存储线程信息,后面唤醒需要获取waiter中的线程
线程t2过来take()
这时候因为松弛度达到2了,会移动tail指针,最终得到如下队列:
线程t3过来put(1)
这时候因为前面take()的时候,队列中已经有了item=null的元素(node!=null),所以会直接进入第2个for循环,然后将值替换进head节点中的item,最后唤醒t1线程,t1线程被唤醒之后,会继续自旋,然后返回拿到t3线程put进来的元素:
最终得到如下队列:
主要经过如下步骤:
- 1、将head节点中的item替换成当前元素1。
- 2、唤醒t1线程。
- 3、t1线程将head节点中的item指向当前自己的Node,并将waiter设置为null。
- 4、t1拿到put进来的元素1返回。
线程t4过来put(2)
这时候主要流程和上面一样,但是因为松弛度达到了2,所以会移动head节点指针,最终得到如下队列:
主要步骤为:
- 1、循环head节点,发现head已经被匹配过了(item=p)。
- 2、继续循环head.next,这时候发现可以匹配上,将head.next中的item设置为2。
- 3、这时候因为松弛度达到2,会将head节点后移。
- 4、将旧head节点的next指向当前自己的节点(forgetNext方法)。
- 5、唤醒线程t2。
- 6、t2线程将新head节点中的item指向当前自己的Node,并将waiter设置为null。
- 7、线程t2拿到元素2返回