本文已参与「新人创作礼」活动,一起开启掘金创作之路。
写在前面
本文主要是针对 ReentrantLock
实现 AQS
的基础上的分析以及对 Condition
的相关分析。
因此建议先了解AQS的实现原理,对 ReentrantLock 原理的理解将更加容易,AQS 详情点击。
一、起源:
1. 什么是可重入锁?
可重入锁
是指当某个线程已经持有了这把锁,但是某个时刻,这个线程还要尝试再次
拿到这把锁,支持这种可重入的实现就是可重入锁;
以 ReentrantLock 可重入锁来看,其 state
表示重入次数,当想要再次拿到这把锁的时候,state + 1
;
当想要释放这把锁的时候state - 1
,因此可以根据 state 是否等于 0 来判断这把锁是否被某个线程锁持有。
2. ReentrantLock的基本用法
先创建 ReentrantLock 对象,然后 lock.lock(),最后 lock.unlock(),即:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}}
使用上比较简单,推荐的做法是在 finally
块中释放锁,因为这样在出现异常的时候可以及时释放锁资源
3. ReentrantLock如何实现等待/通知模式?
关键字 synchronized 与 wait() 和 notify() / notifyAll() 方法相结合可以实现等待/通知
模式,ReentrantLock 也同样可以借助于 Condition
实现等待/通知模式
-
Condition 是 JDK1.5 中出现的技术,使用它有更好的灵活性,比如可以实现多路通知功能,也就是在一个 lock 对象里可以创建多个 Condition (即对象监视器)实例,线程对象可以注册在指定的 Condition 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活
-
在使用
notify()/notifyAll()
方法进行通知时,被通知的线程却是由 JVM 随机选择的;但使用 ReentrantLock 结合 Condition 是可以实现"选择性通知"
在了解 ReentrantLock 实现 AQS 之前,先来看看 AQS 的实现类一般如何去操作...
4. AQS用法
一般要通过 AQS 实现自己的同步器类有以下三步:
- 新建自己的
同步器类
,在内部写一个Sync
类,该类继承AbstractQueuedSynchronizer,即 AQS - 设计同步器类的逻辑,在Sync类里,根据是否独占来重新对应的方法。如果是
独占
,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占
,则重写 tryAcquireShared 和 tryReleaseShared 等方法 - 在自己的同步器类中实现
获取/释放
相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。
二、实现原理
需要注意的的是,state 在 ReentrantLock 的含义表示的是重入次数
- state = 0,表示此锁未被任何线程持有
- state > 0, 表示被当前线程重入持有多次,以后每次释放锁都会在 state 上减 1,直到 state = 0
ReentrantLock 是一种独占模式
,相应的会实现 tryAcquire 和 tryRelease 方法。
在 Condition 中有两个名词需要做区分
条件等待队列
,这个队列是由每个 condition 实例自己维护的,也就是说,如果有两个 condition 实例,也就有两个条件等待队列同步队列
:指的是 AQS 中的 FIFO
等待与唤醒:
condition.await
:等待操作是将节点放入条件等待队列condition.signal
:唤醒操作是将节点从条件等待队列中移到同步队列中,等待获取资源
另外,ReentrantLock 大部分实现都是由 AQS 完成,在上篇博文中已经对 AQS 做了详细分析,因此这里不在过多重复分析...
1. 内部对象Sync实现AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
// 非公平锁,tryAcquire方法由子类实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 拿到当前锁对象的重入次数
int c = getState();
// 如果等于0说明该锁对象没有被任何对象持有
// 这个时候等待队列可能是有等待节点的,只是恰好锁资源在此刻被释放了
if (c == 0) {
// 这里尝试去抢这把锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果此锁被当前对象持有,也就是重入操作,累加state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 尝试释放锁资源
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 检查是不是当前线程获得了锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果重入次数等于0了,说明完全释放了这把锁,其他线程可以获取这把锁了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// 判是否独占模式
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 创建Condition对象
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 重入次数
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// state=0表示此锁未被占用
final boolean isLocked() {
return getState() != 0;
}
}
//
具体实现逻辑已经在源码中注释,简单来说,子类(这里的 Sync)需要实现 AQS 提供的抽象方法,来达到自己个性化的需求。
2. NonfairSync 非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 尝试获取锁
// 这个时候等待队列可能是还有等待节点的,这里取尝试抢一下;
// 如果锁资源这个时候刚好被释放了,这里是有可能抢成功的
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 既然没有抢成功,那就老老实实取获取锁资源
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//
公平锁
与非公平锁
的最大区别在于,非公平锁会尽可能的去抢占资源
(尽管等待队列存在很多等待节点)。
而公平锁,如果等待队列里存在等待节点,那它是不会去抢占资源的,放进队列,然后按先进先出
的顺序去获取资源。
3. FairSync 公平锁实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取锁,如果拿不到会进入阻塞队列中等待
final void lock() {
acquire(1);
}
// 尝试拿取锁,成功则返回true,失败返回false
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 拿到重入次数
int c = getState();
// 说明这把锁未被任何线程持有,可以尝试获取锁
if (c == 0) {
// 和非公平锁的唯一区别是,这里多了hasQueuedPredecessors判断条件
// 意思是:首先判断在等待队列里面没有任何等待节点,它才会尝试取获取资源,
// 否则的话,就不去争抢锁资源了,毕竟是先来先服务嘛(保证公平性)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // CAS设置状态值
// 说明获取锁资源成功了,在锁对象中设置exclusiveOwnerThread=当前线程,表明此锁被当前线程锁住了
setExclusiveOwnerThread(current);
return true;
}
}
// 判断是否当前线程持有,是的话,就是重入持有,改变state的值
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
//
4. Condition
在特定条件上等待锁资源
来看个例子:
/**
* 使用多个Condition实现通知部分线程
*/
public class ReentrantLockExample {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public void awaitA() {
lock.lock();
try {
System.out.println("start awaitA at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
conditionA.await();
System.out.println("end awaitA at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void awaitB() {
lock.lock();
try {
System.out.println("start awaitB at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
conditionB.await();
System.out.println("end awaitB at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signalAll_A() {
lock.lock();
try {
System.out.println("signalAll_A at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
public void signalAll_B() {
lock.lock();
try {
System.out.println("signalAll_B at " + System.currentTimeMillis()
+ " ThreadName:" + Thread.currentThread().getName());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread a = new Thread(() -> {
example.awaitA();
});
a.setName("A");
a.start();
Thread b = new Thread(() -> {
example.awaitB();
});
b.setName("B");
b.start();
example.signalAll_A();
/**
* print: 从输出结果可以看出只有线程A被唤醒了
*
* start awaitA at 1596380331589 ThreadName:A
* start awaitB at 1596380331590 ThreadName:B
* signalAll_A at 1596380331590 ThreadName:main
* end awaitA at 1596380331590 ThreadName:A
*/
}
}
//
在以上例子中,想要实现的效果是使用多个 Condition 实现通知部分线程,也就是将唤醒粒度
变小。
比如说,我现在有 10 个线程,定义了 5 个 condition 对象,每个 condition 上都注册两个线程, 假设某种情况下,10 线程都通过 await 阻塞了,这个时候假如 conditionA 的两个线程可以被唤醒处理其业务了(有可用资源),这个时候我可以做到只唤醒 conditionA 上两个线程,其他线程仍然在阻塞状态;这样就做到了精准唤醒
了。
来看看 Condition 的实现原理:
4.1 newCondition
创建 Condition 对象,这里的 ConditionObject 是 AQS 的内部类
final ConditionObject newCondition() {
return new ConditionObject();
}
4.2 await
AbstractQueuedSynchronizer#ConditionObject.await()
也就是我们例子中通过 conditionA.await() 进入阻塞
状态,等待其他线程调用 signalAll 或者 signal唤醒
public final void await() throws InterruptedException {
// 如果当前线程已经被中断了,就响应中断(也就是抛出异常)
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程包装成Node放入条件等待队列的队尾
Node node = addConditionWaiter();
// 同时还有释放当前线程已获取的资源
int savedState = fullyRelease(node);
int interruptMode = 0;
// 如果当前节点不在同步队列里,那就将线程挂起
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 说明当前节点已经从条件队列移到了同步队列中(也就是从await状态被signal唤醒之后,可以尝试获取锁资源进行后续操作了)
// 从上面被挂起的地方被唤醒之后,尝试去获取锁资源,如果获取失败,那就会进入等待队列中
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 记录中断信息
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//
主要操作
- 将当前线程包装成 node 节点之后放入条件队列
- 释放当前线程占用的资源
- 挂起当前线程
- 当线程被 signal 或者 signalAll 唤醒之后,从条件队列移到同步队列,并尝试获取锁资源
4.2.1 addConditionWaiter
AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter
将当前线程包装成 Node 放入条件等待队列
的队尾
/**
* Adds a new waiter to wait queue.
* @return its new wait node
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
// 从条件等待队列中移除取消的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//
4.2.2 unlinkCancelledWaiters
AbstractQueuedSynchronizer.ConditionObject#unlinkCancelledWaiters
从条件等待队列中移除
被取消的节点
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;
}
}
//
4.2.3 fullyRelease 释放资源
AbstractQueuedSynchronizer#fullyRelease
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 拿到当前线程锁占用的资源,然后释放
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
//
4.2.4 isOnSyncQueue
AbstractQueuedSynchronizer#isOnSyncQueue
判断当前节点是否在同步器队列中,如果是的话说明当前节点正在等待获取资源
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 注意:在条件等待队列用的nextWaiter来表示下一个节点
// 因此如果next不为空说明已经在同步队列里面了
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
return findNodeFromTail(node);
}
// 从队尾遍历找到这个节点
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
//
4.3 signal 唤醒在条件等待队列中的节点
AbstractQueuedSynchronizer.ConditionObject#signal
public final void signal() {
// 必须满足是独占的模式下
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 进行唤醒
doSignal(first);
}
4.3.1 doSignal
AbstractQueuedSynchronizer.ConditionObject#doSignal
// 尝试唤醒条件等待队列中的一个节点,直到唤醒一个为止
// 遇到取消节点就跳过,如果到队列末端都没有成功,那就结束,说明这个队列没有可唤醒的节点
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// AbstractQueuedSynchronizer#transferForSignal
final boolean transferForSignal(Node node) {
// 在条件等待队列中有两种状态,要么是CONDITION,要么是CANCELED
// 如果不是CONDITION,说明是被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 从条件等待队列中进入到同步队列
Node p = enq(node);
// enq返回的是当前接的前驱节点,如果前驱节点被取消了或者我们设置前驱节点为SIGNAL状态失败了
// 那我们就自己唤醒当前节点(将前驱节点设置为SIGNAL,最后也是LockSupport.unpark进行唤醒,只不过是在同步队列中等待前驱节点的唤醒而已)
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
//
signalAll 和 signal 原理类似,只不过一个是唤醒所有在当前 condition 上等待的节点,另一个是只唤醒一个,这里不在赘述。
相关文档: