1. 产生背景
在Java中已经有内置锁synchronized的情况下,为什么还需要引入ReentranLock呢?
synchronized是基于 JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
基于这样一个背景,Doug Lea在JDK1.5中提供了API层面的互斥锁ReentranLock,实现了可重入、可中断、公平锁等特性。在synchronized优化之前,synchronized的性能比起ReentranLock还是有差距的。
2. 简单使用
public class ReentrantLockSimple {
private ReentrantLock lock = new ReentrantLock();
public void syncIncr() {
try {
lock.lock();
// todo 模拟业务,不释放锁
Thread.sleep(10_000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockSimple simple = new ReentrantLockSimple();
// 模拟并发场景
new Thread(simple::syncIncr).start();
new Thread(simple::syncIncr).start();
}
}
3. 源码分析
3.1 ReentrantLock结构
首先,RentrantLock对象结构,有个大致的了解。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
// 同步控制器(AQS)
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
// 非公平锁(实现)
static final class NonfairSync extends Sync {
...
}
// 公平锁(实现)
static final class FairSync extends Sync {
...
}
// 构造器:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
3.2 加锁 lock.lock()
其实,ReentrantLock中的方法都是由成员对象sync完成的。
所以,我们直接进入 NonfairSync.lock()
final void lock() {
// CAS设置状态值,如果成功,当前线程设为独占线程,意味着获取到了锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
接下来,分析 acquire()中的三个重点方法:
- tryAcquire():尝试获取锁,由子类重写(这里实现了可重入、公平锁特性);
- addWaiter():AQS中维护了一个链表,将当前线程包装成一个Node节点,入队;
- acquireQueued():以独占不可中断模式获取已经在队列中的线程。
3.2.1 tryAcquire
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 获取当前锁的状态
int c = getState();
if (c == 0) {
// 公平锁在下面的if中还必须要满足 !hasQueuedPredecessors(),字面意是队列中没有等候者。
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程已经获取了锁,重复加锁~
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;
}
当前方法无论是获取到了锁,还是重复加锁,都是返回true。上面的 acquire()就会直接结束,不会继续执行后续的入队操作。
非公平锁的特性就是在这个方法中体现出来的(其实lock方法中也是直接获取锁,更直接)。
3.2.2 addWaiter
注意,当前方法调用时,传入了一个参数:Node.EXCLUSIVE(独占模式)。这个参数实际上是起到了一个标识的作用,有两种类型:独占、共享。
private Node addWaiter(Node mode) {
// 将当前线程包装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
// 当前Node节点加入到队尾,并与前tail建立引用关系
Node pred = tail;
if (pred != null) {
node.prev = pred;
// CAS设置当前Node为队尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 死循环,直到入队成功
enq(node);
return node;
}
private Node enq(final Node node) {
// 循环调用,直至return
for (;;) {
Node t = tail;
// 初始化,创建一个新的Node节点,占位队列中head和tail
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// CAS设置队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter()的职责很简单,就是将当前线程包装成一个Node节点,然后设置为队尾(必要时初始化队列),通过死循环的形式保证一定入队成功。
下面,大致了解一下Node的结构(AbstractQueuedSynchronizer的内部类)
static final class Node {
/*
* 线程的等候状态(初始化为0):
* CANCELLED(1):表明当前节点已取消;
* SIGNAL(-1):表明当前节点等待被唤醒unparking;
* CONDITION(-2):表明当前节点需要达到一定条件;
* PROPAGATE(-3):传播
*/
volatile int waitStatus;
// 前后驱节点:标准的链表结构
volatile Node prev;
volatile Node next;
// 当前持有线程
volatile Thread thread;
}
3.2.3 acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 线程是否中断
boolean interrupted = false;
// 对于任何一个线程来说,这个死循环一般只会执行三次
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
// 如果前驱节点就是队头,尝试获取锁
if (p == head && tryAcquire(arg)) {
// 当前节点设置为队头
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
代码执行到这里,说明节点(当前线程)已经成功入队。如果当前节点就是第一位候选者,就会尝试去获取锁。但是锁有可能还没有释放掉(state != 0),获取锁失败,就会阻塞当前线程。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 注意,这里获取的是前驱节点的等候状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前驱节点的状态是信号通知,意味着当前节点就可以安全的阻塞了
*/
return true;
if (ws > 0) {
/*
* 如果前驱节点的状态是取消,则不断的向前查找,执行某个前驱节点的状态不大于0
* 将这个前驱节点与当前节点node建立前后引用,解绑了两者中间所有已经取消的节点
*/
do {
/**
* 下面的一行代码等同于:
* pred = pred.prev;
* node.prev = pred;
*/
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 代码执行到这里,说明状态值必定是 0 或者 -3(PROPAGATE),表明当前节点是需要被唤醒的
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
相信大家也看到了,这个方法中有段代码的写法太(bi)过(jiao)精(e)简(xin)。
其中,红色方块的pred和N1状态都是已取消的(状态值为1)。
首先,执行if判断时,发现pred的等候状态是已取消的,执行do代码块,将pred指向N1节点。执行while判断,发现N1节点的状态也是已取消的,再次执行do代码块,将pred指向N2节点。执行while判断,N2节点状态并不是已取消,循环结束。而在循环结束之前node的前驱指针已经指向了N2,循环结束后再将N2的后驱指针指向node,两者就建立了双向关联。
细心的你一定发现了,在acquireQueued()方法中的死循环上注释了一般只会执行三次,那么为什么说会执行三次呢?
假设现在lock锁已经被某个线程获取了,并且还在执行同步代码块,没有来得及释放锁。这时,线程A也来执行了业务方法,然后尝试获取锁,必然获取失败进而执行入队操作。此时,由当前线程包装的Node节点占据队尾,队头是初始化的一个“空”节点。
参数pred 实际上就是head节点(状态值为0)。第一次循环,执行else逻辑,将前驱节点pred的状态值设置为SIGNAL,注意返回的是false,这将导致当前方法所在if判断直接结束;第二次循环,首个if判断生效,返回true,执行parkAndCheckInterrupt(),阻塞线程;直到线程被唤醒后,开启第三次循环,获取锁成功,直接返回,结束死循环。
注意:以上描述,队列中不存在已取消的节点并且锁不会被争抢,故而说是一般情况下。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
不得不说,AQS的方法名取的还是很贴切的,基本上见名知意了。阻塞当前线程并返回线程中断状态!
我们先来了解一下 LockSupport.park(this)的功能点:
- 阻塞当前线程的执行,且不会释放当前线程占有的锁资源;
- 可以被另一个线程调用 LockSupport.unpark()方法唤醒;
- 底层调用 Unsafe的native方法。
// java.util.concurrent.locks.LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L); //打上断点,debug执行示例代码,10秒钟后线程被唤醒
setBlocker(t, null);
}
3.2.4 总结
ReentranLock的一套加锁流程总结下来,就是尝试获取锁,获取成功,更新锁状态、设置独占线程;获取失败,将当前线程包装成一个Node节点,加入到AQS内部维护的一个链表的尾部,最后阻塞当前线程,直到被唤醒,再次尝试获取锁(非公平锁,存在被争抢的可能性)。
3.3 解锁 lock.unlock()
加锁时自我阻塞的线程是如何被唤醒的,触发的机制又是怎样的?
接下来,让我们一步步的分析ReentranLock的另一个重要组件。
// ReentranLock
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒等候线程
unparkSuccessor(h);
return true;
}
return false;
}
解锁逻辑主要分为两部分:释放锁、唤醒阻塞线程。
3.3.1 tryRelease
protected final boolean tryRelease(int releases) {
// 本次解锁后,剩余锁次数
int c = getState() - releases;
// 如果解锁线程非独占线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 锁释放,清空独占线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新锁次数
setState(c);
return free;
}
前后呼应了,可重入的特性!
需要注意的是加锁和解锁的次数要一致,不然将会导致队列中的等候线程无法被唤醒。
3.3.2 unparkSuccessor
private void unparkSuccessor(Node node) {
/*
* 如果状态为负数(即,可能需要信号),尝试清除预期信号
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 预唤醒节点(head的后驱节点)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
预唤醒的节点s 是node的后驱节点(FIFO)。
但是如果s 为空或者已取消时,就需要从队列中找出一个正常的节点。怎么找呢? 以队尾节点为起始点,向前遍历,最终s指向的是队列中最靠近 node的一个正常节点。
随后,通过调用 LockSupport.unpark()的方式,唤醒 s节点持有的线程。
至此,ReentranLock一套完整的加锁解锁流程就分析完毕了~