在Java 1.5 JUC之前,我们一直使用synchronized、搭配Object.wait和Object.notify实现线程间的协作。
在Java SE5后,Java提供了Lock接口,相对于synchronized而言,Lock提供了条件队列Condition,对线程的等待、唤醒操作更加详细和灵活。
自此 synchronized和ReentrantLock一起构成了Java并发世界里最重要的两把锁。
关于 ReentrantLock ,我们必须弄明白Java有了synchronized,为何再造ReentrantLock ?
这是理解ReentrantLock 和synchronized 区别的基础。
上篇 细聊ReentrantLock之Condition 我们提到,synchronized和 ReentrantLock 二者绝不是什么“替代”关系,性能也绝不是衡量选择二者的主要原因。
ReentrantLock 有别于 synchronized 隐式锁的三个特性:能够响应中断、支持超时和非阻塞地获取锁。
这三个关键特征,对应Lock源码的三个方法:
// 支持中断的API
void lockInterruptibly()
throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
而解决同步问题,ReentrantLock 采用的是内部Condition类。
Condition提供了类似于Object.wait/notify 等待/通知的API,用于线程间协作:
await() :阻塞当前线程,直到被signal()
await(long time, TimeUnit unit) :有时限的阻塞当前线程,直到被signal()
awaitNanos(long nanosTimeout) :有时限的阻塞当前线程,直到被signal()返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
awaitUninterruptibly() :阻塞当前线程,直到被signal()【注意:该方法对中断不敏感】。
awaitUntil(Date deadline) :阻塞当前线程,直到被signal()。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
这些方法,其实就两组,Condition.await/signal 分别和Object.wait/notify想对应。
分成两组,await()其实就是用于阻塞当前线程,将当前线程加入条件队列,signal() 就是用于唤醒和通知。
Condition.await等待的三种形式:可中断,不可中断和定时。就是为了更灵活的支持业务场景。和ReentrantLock 的三个特性:能够响应中断、支持超时和非阻塞地获取锁 相对应。
可以说Lock 是继synchronized之后,重新设计的一把锁,全面弥补 synchronized 的问题。为解决死锁问题,提供了强有力的支持。
Condtion源码实现
上篇 细聊ReentrantLock之Condition 我们从宏观角度了解了ReentrantLock的Condition,并提出Condition本质上是个队列,本篇将会详细分析RetrantLock的Condition源码,从源码层面看Condition是如何实现条件队列的。
获取一个Condition必须要通过Lock的newCondition()方法。该方法定义在接口Lock下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition为一个接口,其下仅有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。定义如下:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//头节点
private transient Node firstWaiter;
//尾节点
private transient Node lastWaiter;
public ConditionObject() {
}
/** 省略方法 **/
}
Node的定义如下:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
Node中包含链表的后继节点next指针,同时包含当前Node所代表的当前线程,可见Node是对线程的包装。
Node定义与AQS的CLH同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized.Node静态内部类)。
从上面代码可以看出Condition拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用await()方法,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:
Condition的队列结构比CLH同步队列的结构简单些,新增过程较为简单只需要将原尾节点的nextWaiter指向新增节点,然后更新lastWaiter即可。
调用Condition的await()方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。当从await()方法返回时,当前线程一定是获取了Condition相关连的锁。
public final void await() throws InterruptedException {
// 当前线程中断
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列
Node node = addConditionWaiter();
//释放锁
long savedState = fullyRelease(node);
int interruptMode = 0;
/**
* 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
* 直到检测到此节点在同步队列上
*/
while (!isOnSyncQueue(node)) {
//线程挂起
LockSupport.park(this);
//如果已经中断了,则退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//竞争同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理下条件队列中的不是在等待条件的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
核心逻辑是:首先将当前线程新建一个Node节点同时加入到条件队列中(addConditionWaiter()),然后释放当前线程持有的同步状态。然后则是不断检测该节点代表的线程释放出现在CLH同步队列中(收到signal信号之后就会在AQS队列中检测到),如果不存在则一直挂起,否则参与竞争同步状态。
加入条件队列(addConditionWaiter())源码如下:
private Node addConditionWaiter() {
Node t = lastWaiter; //尾节点
//Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点
if (t != null && t.waitStatus != Node.CONDITION) {
//清除条件队列中所有状态不为Condition的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//当前线程新建节点,状态CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
/**
* 将该节点加入到条件队列中最后一个位置
*/
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
该方法主要是将当前线程加入到Condition条件队列中。当然在加入到尾节点之前会清除所有状态不为Condition的节点。
fullyRelease(Node node),负责释放该线程持有的锁。
final long fullyRelease(Node node) {
boolean failed = true;
try {
//节点状态--其实就是持有锁的数量
long savedState = getState();
//释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
isOnSyncQueue(Node node):如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回true
final boolean isOnSyncQueue(Node node) {
//状态为Condition,获取前驱节点为null,返回false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//后继节点不为null,肯定在CLH同步队列中
if (node.next != null)
return true;
return findNodeFromTail(node);
}
unlinkCancelledWaiters():负责将条件队列中状态不为Condition的节点删除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
调用Condition的signal()方法,将会唤醒在Condition等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。(移到CLH同步队列中,需要重新获得ReentrantLock锁)
public final void signal() {
//检测当前线程是否为拥有锁的独
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//头节点,唤醒条件队列中的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first); //唤醒
}
该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒条件队列中的头节点。
doSignal(Node first):唤醒头节点
private void doSignal(Node first) {
do {
//修改头结点,完成旧头结点的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal(Node first)主要是做两件事:
1.修改头节点,
2.调用transferForSignal(Node first) 方法将节点移动到CLH同步队列中。transferForSignal(Node first)源码如下:
final boolean transferForSignal(Node node) {
//将该节点从状态CONDITION改变为初始状态0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
Node p = enq(node);
int ws = p.waitStatus;
//如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
整个signal()通知的流程如下:
1、判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
2、如果线程已经获取了锁,则将唤醒条件队列的首节点。
3、唤醒首节点是先将条件队列中的头节点移出,然后调用AQS的enq(Node node)方法将其安全地移到CLH同步队列中。
4、最后判断如果该节点的同步状态是否为Cancel,或者修改状态为Signal失败时,则直接调用LockSupport唤醒该节点的线程。
总结
一个线程获取锁后,通过调用Condition的await()方法,会先将当前线程包装成Node加入到条件队列中,然后释放锁,最后通过isOnSyncQueue(Node node)方法不断自检看节点是否已经在CLH同步队列了,如果是则尝试获取锁,否则一直挂起。
当线程调用signal()方法后,程序首先检查当前线程是否获取了锁,然后通过doSignal(Node first)方法将Condition条件队列的头结点转移到CLH、并唤醒。被唤醒的线程,将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。
ps:
ReentrantLock 随Java 1.5 JUC并发包发布,一举成为Java并发世界两把锁之一。要看懂底层实现,实则并不容易。
ReentrantLock 底层是借助AbstractQueuedSynchronized 同步器框架,AbstractQueuedSynchronized(AQS)也是大厂面试的重点。
AQS 更是JUC Java并发包所有工具类实现的基础。
AQS 作为JUC 同步器框架 的抽象父类 ,要读懂AQS的源码只有一般Java编程的功底恐怕很难读懂(AQS源码本身就很晦涩难懂)。AQS底层则是基于CLH队列实现的,就是文中提到的CLH。
本篇重点并非AQS和CLH,不深入探究,后续会陆续涉及更新。
但需要强调的是:ReentrantLock不止一个队列,ReentrantLock支持条件队列,也就是Condition,Condition条件队列 和 实现ReentrantLock的CLH队列,是两个队列,不要弄混。这里先埋个伏笔,后续会就此深入展开。
pss:
Java并发编程,本身就是一个庞大的世界,要理解的比较透彻本身就不容易。而且,在各编程语言中,并发领域,Java都一直远远走在前面。基于Java深入切入,其他的都能触类旁通。
欢迎关注 公众号 架构道与术(ToBeArchitecturer)、学习更多干货~
推荐阅读
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
02 | Java内存模型:看Java如何解决可见性和有序性问题