Condition的使用
============
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。 Condition的使用方式比较简单,需要注意在调用方法前获取锁。
package com.joonwhee.five;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
-
Condition接口与示例
-
@author JoonWhee
-
@Date 2018年2月11日
-
@param
*/
public class BoundedQueue {
// 线程池
private static ExecutorService THREAD_POOL = new ThreadPoolExecutor(2, 4, 60, TimeUnit.MINUTES,
new LinkedBlockingQueue(1000), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
private Object[] items; // 对象数组
// 添加的下标,删除的下标和数组当前数量
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock(); // 定义一个可重入锁
private Condition notEmpty = lock.newCondition(); // 添加一个Condition
private Condition notFull = lock.newCondition(); // 添加一个Condition
public BoundedQueue(int size) {
items = new Object[size];
}
/**
-
添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
-
@param t
-
@throws InterruptedException
*/
public void add(T t) throws InterruptedException {
lock.lock(); // 获取锁
try {
while (count == items.length) { // 如果数组满了,notFull进入等待
System.out.println(“items满了,add方法进入等待.”);
notFull.await(); // 等待remove方法里的notFull.signal()
}
items[addIndex] = t; // item添加对象
if (++addIndex == items.length) // 调整数组索引,避免越界
addIndex = 0;
++count; // count+1,代表添加了一个对象
notEmpty.signal(); // 走到这里,数组里至少有1个对象,必不为空,因此唤醒notEmpty
} finally {
System.out.println("add: " + t);
lock.unlock(); // 释放锁
}
}
/**
-
由头部删除一个元素,如果数组空,则删除线程进入等待状态,
-
直到有新添加元素(注意这里并没有真的删除元素,只是把count-1当作是删除)
-
@return
-
@throws InterruptedException
*/
@SuppressWarnings(“unchecked”)
public T remove() throws InterruptedException {
lock.lock(); // 获取锁
try {
while (count == 0) { // 如果数组为空,notEmpty进入等待
System.out.println(“items为空,remove方法进入等待.”);
notEmpty.await(); // 等待add方法里的notEmpty.signal()
}
Object x = items[removeIndex]; // item移除对象(假移除)
if (++removeIndex == items.length) // 调整数组索引,避免越界
removeIndex = 0;
–count; // count-1,代表移除了一个对象
notFull.signal(); // 走到这里,数组里至少有1个空位,必不为满,因此唤醒notFull
return (T) x;
} finally {
System.out.println(“remove”);
lock.unlock(); // 释放锁
}
}
public static void main(String args[]) throws InterruptedException {
int count = 3; // 可以加大数组的size来看更多的过程
BoundedQueue bq = new BoundedQueue(count);
// 开启一个线程执行添加操作
THREAD_POOL.submit(new Callable() {
public Object call() throws InterruptedException {
for (int i = 0; i < count * 2; i++) {
bq.add(i);
Thread.sleep(200); // 通过睡眠来制造添加和移除的速度差
}
return null;
}
});
// 开启一个线程执行移除操作
THREAD_POOL.submit(new Callable() {
public Object call() throws InterruptedException {
Thread.sleep(1000);
for (int i = 0; i < count * 2; i++) {
bq.remove();
Thread.sleep(50); // 通过睡眠来制造添加和移除的速度差
}
return null;
}
});
}
}
输出如下,由于调用remove方法的线程先睡眠了1秒,所以,add方法会先将item数组填满,填满后notFull进入等待。之后,remove方法的线程醒来开始进行移除,当移除之后会唤醒notFull,此时add和remove是并发操作的,但是由于remove的速度更快(通过sleep控制,add每次要睡200毫秒,remove只要50毫秒),所以items必然会被移除到为空,此时notEmpty进入等待,直到add方法往item添加了对象,如此反复。
重要入口方法
======
Condition的实现主要包括:条件队列、等待和通知。其中条件队列放的是AQS里的Node数据结构,使用nextWaiter来维护条件队列。等待和通知共有7个方法。
-
signal():唤醒该条件队列的头节点。
-
signalAll():唤醒该条件队列的所有节点。
-
awaitUninterruptibly():等待,此方法无法被中断,必须通过唤醒才能解除阻塞。
-
await():当前线程进入等待。
-
awaitNanos(long):当前线程进入等待,有超时时间,入参的单位为纳秒。
-
awaitUntil(Date):当先线程进入等待,直到当前时间超过入参的时间。
-
await(long, TimeUnit):当前线程进入等待,有超时时间,入参可以自己设置时间单位。
这些方法其实大同小异,因此本文只对常用的signal()、signalAll()和await()方法展开详解。搞懂了这3个方法,搞懂其他几个方法也基本没什么阻碍。
基础属性
====
Condition的实现是ConditionObject,而ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为条件队列),该队列是Condition对象实现等待/通知功能的关键。
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter; // 条件队列的头节点
/** Last node of condition queue. */
private transient Node lastWaiter; // 条件队列的尾节点
/**
- Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
通过源码可知,条件队列的节点使用的是AQS的Node数据结构。(Node的数据结构见:Node的数据结构)
另外,由于ConditionObject是AQS的内部类,因此必然和AQS是有很多关联的,因此看本文之前必须先了解AQS的实现原理。(如果你对AQS不熟悉,可以参考我的另一篇文章:Java并发:AbstractQueuedSynchronizer详解(独占模式))
条件队列的基本数据结构如下图中的“条件队列”:
await方法
=======
public final void await() throws InterruptedException { // 阻塞当前线程,直接被唤醒或被中断
if (Thread.interrupted()) // 如果当前线程被中断过,则抛出中断异常
throw new InterruptedException();
Node node = addConditionWaiter(); // 添加一个waitStatus为CONDITION的节点到条件队列尾部
int savedState = fullyRelease(node); // 释放操作。我们知道只有在拥有锁(acquire成功)的时候才能调用await()方法,因此,调用await()方法的线程的节点必然是同步队列的头节点。所以,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的条件队列中。
int interruptMode = 0; // 0为正常,被中断值为THROW_IE或REINTERRUPT
while (!isOnSyncQueue(node)) { // isOnSyncQueue:判断node是否在同步队列(注意和条件队列区分。调用signal方法会将节点从条件队列移动到同步队列,因此这边就可以跳出while循环)
LockSupport.park(this); // node如果不在同步队列则进行park(阻塞当前线程)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 检查线程被唤醒是否是因为被中断,如果是则跳出循环,否则会进行下一次循环,因为被唤醒前提是进入同步队列,所以下一次循环也必然会跳出循环
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // acquireQueued返回true代表被中断过,如果中断模式不是THROW_IE,则必然为REINTERRUPT(见上面的checkInterruptWhileWaiting方法)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // 移除waitStatus为CANCELLED的节点
if (interruptMode != 0) // 如果跳出while循环是因为被中断
reportInterruptAfterWait(interruptMode); // 则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程
}
-
如果当前线程被中断过,则抛出中断异常。
-
调用addConditionWaiter方法(详解见下文addConditionWaiter方法)添加一个waitStatus为CONDITION的节点到条件队列尾部。
-
调用fullyRelease方法(详解见下文fullyRelease方法)释放锁。
-
调用isOnSyncQueue方法(详解见下文isOnSyncQueue方法)来阻塞线程,直到被唤醒或被中断。
-
调用acquireQueued方法(详解见acquireQueued方法详解)来尝试获取锁,并判断线程跳出while循环是被唤醒还是被中断。
-
如果跳出while循环是因为被中断,则根据interruptMode,选择抛出InterruptedException 或 重新中断当前线程。
addConditionWaiter方法
private Node addConditionWaiter() { // 添加一个waitStatus为CONDITION的节点到条件队列尾部
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION)
t = lastWaiter; // 将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 以当前线程新建一个waitStatus为CONDITION的节点
if (t == null) // t为空,代表条件队列为空
firstWaiter = node; // 将头节点赋值为node
else
t.nextWaiter = node; // 否则,队列不为空。将t(原尾节点)的后继节点赋值为node
lastWaiter = node; // 将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的
return node;
}
-
如果条件队列的尾节点不为null并且waitStatus不为CONDITION,则调用unlinkCancelledWaiters方法(详解见下文unlinkCancelledWaiters方法)移除waitStatus不为CONDITION的节点(条件队列里的节点waitStatus都为CONDITION),并将t赋值为移除了waitStatus不为CONDITION后的尾节点(上面进行了移除操作,因此尾节点可能会发生变化)
-
以当前线程新建一个waitStatus为CONDITION的节点。
-
如果t为空,代表条件队列为空,将头节点赋值为node;否则,队列不为空。将t(原尾节点)的后继节点赋值为node。
-
最后将node赋值给尾节点,即将node放到条件队列的尾部。这里没有用CAS来保证原子性,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
unlinkCancelledWaiters方法
private void unlinkCancelledWaiters() { // 从条件队列移除所有waitStatus不为CONDITION的节点
Node t = firstWaiter; // t赋值为条件队列的尾节点
Node trail = null;
while (t != null) {
Node next = t.nextWaiter; // 向下遍历
if (t.waitStatus != Node.CONDITION) { // 如果t的waitStatus不为CONDITION
t.nextWaiter = null; // 断开t与t后继节点的关联
if (trail == null) // 如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点
firstWaiter = next;
else
trail.nextWaiter = next; // 否则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)
if (next == null)
lastWaiter = trail;
}
else
trail = t; // 如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION
t = next; // t指向下一个节点
}
}
-
将t赋值为条件队列的尾节点 。
-
从t开始遍历整个条件队列。
-
如果t的waitStatus不为CONDITION,则断开t与t后继节点的关联。
-
如果trail为null,则将firstWaiter赋值为next节点,此时还没有遍历到waitStatus为CONDITION的节点,因此直接移动firstWaiter的指针即可移除前面的节点。
-
如果trail不为null,则将trail的后继节点设为next节点。此时,trail节点到next节点中的所有节点被移除(包括t节点,但可能不止t节点。因为,trail始终指向遍历过的最后一个waitStatus为CONDITION,因此只需要将trail的后继节点设置为next,即可将trail之后到next之前的所有节点移除)
-
如果t的waitStatus为CONDITION,则将trail赋值为t,trail始终指向遍历过的最后一个waitStatus为CONDITION。
-
最后将 t指向下一个节点,准备开始下一次循环。
例子图解过程:
fullyRelease方法
final int fullyRelease(Node node) { // 释放锁
boolean failed = true;
最后
很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。
我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。
不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~