上图是我引用的图片。绿色的虚线代表实现接口,红色线代表内部类(比如Sync是ReentrantLock的内部类),蓝色线代表继承。
先从构造器开始看,默认为非公平锁实现
public ReentrantLock() {
sync = new NonfairSync();
}
NonfairSync 继承自 AQS
reentrantLock.lock()
流程
public void lock() {
sync.lock();// 这里看sync的运行时类,先考虑非公平锁,即NonfairSync
}
final void lock() {
if (compareAndSetState(0, 1))// AQS中,用cas方式修改state从0->1,0代表为加锁,1代表加锁1次
setExclusiveOwnerThread(Thread.currentThread());// 设置当前线程为锁的持有者
else// 如果cas失败,说明有线程曾经上过锁,注意:之前上锁的那个线程也有可能是当前线程(重入)
acquire(1);// AQS中的方法
}
// 能进入这个方法,说明当前的state已经不是0了
public final void acquire(int arg) {// arg = 1
if (!tryAcquire(arg) &&// 尝试获得锁,如果成功,!tryAcquire(arg) = false,短路与直接退出
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 尝试获取锁失败,执行acquireQueued
selfInterrupt();// 自我打断
}
tryAcquire(arg)
逻辑,这个方法是AQS中定义的,希望子类去重写的方法,下面是AQS中的tryAcquire
实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
显然AQS是不希望你调用到AQS中的tryAcquire
的。
查找子类实现,由于此时this的运行时类是NonfairSync(非公平锁),所以去NonfairSync中去找tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) { // acquire = 1
final Thread current = Thread.currentThread();// 当前线程
int c = getState();// 获取状态,这里已经不是0了
if (c == 0) {// 如果其他线程已经释放锁了,会把state变成0,释放锁的逻辑后面说,这里假设没有释放锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {// 如果当前线程是锁的持有者,锁重入了
int nextc = c + acquires;// 下一个state += 1
if (nextc < 0) // 如果当前state = (2^31) - 1那么 state + 1 = -(2^31),证明整数越界
throw new Error("Maximum lock count exceeded");// 这里肯定不会
setState(nextc);// 更新后的值就是2了,证明当前线程加了2次锁,锁重入计数为2
return true;
}
return false;// 如果当前state不为0(未释放锁),并且当前线程也不是锁目前的持有者,尝试获得锁失败
}
所以尝试获得锁成功,要么是因为锁已经被其他线程释放了(state重新改成0)
,要么就是当前线程就是锁的持有者(锁重入)
尝试获得锁失败,是因为锁的持有者不是当前线程
,并且持有锁的那个线程还没有释放锁
if (!tryAcquire(arg) &&// 这里如果获得锁成功,就直接返回了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 获取锁失败,这个方法中会让线程阻塞
selfInterrupt();
}
我们先看一下addWaiter(Node.EXCLUSIVE)
的逻辑,Node.EXCLUSIVE
就是Node定义的一个常量表示独占锁
private Node addWaiter(Node mode) {// 节点的模式,AQS$Node规定了两种模式,独占锁和共享锁
Node node = new Node(Thread.currentThread(), mode);// 创建一个节点,这个节点保存了当前线程
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;// 获取尾部节点
if (pred != null) {// 第一次tail就是空
node.prev = pred;// 如果不是第一次进入addWaiter,先把当前节点的钱去接待你指向tail
if (compareAndSetTail(pred, node)) {// 把tail用cas方式设置成新创建的node
pred.next = node;// 将以前尾节点的next域指向当前新创建的节点
return node;// 返回新创建的节点
}
}
enq(node);// ⬇看下面,第一次tail是空,就会走这个方法
return node;// 返回新创建的节点
}
private Node enq(final Node node) {// 这个节点就是新创建的那个节点
for (;;) {// 死循环
Node t = tail;// 当前tail,仍然是空
if (t == null) { // Must initialize 必须初始化
if (compareAndSetHead(new Node()))// cas方式设置头节点,头节点不保存任何线程和模式
tail = head;// 让tail指向头节点,此时head和tail都指向同一个节点
} else {// 多个线程同时调用enq时,casHead操作只会有一个线程成功,对于cas失败的线程就会进入下一次循环,下一次循环就会走else,因为tail已经不是空了。cas成功的线程也要进入下一次循环把自己的node挂到tail后面
node.prev = t;// 当前节点的前驱节点指向tail
if (compareAndSetTail(t, node)) {// cas修改tail的指向,如果失败进入下一次循环
t.next = node;// 之前的tail的next域指向当前节点
return t;// 返回之前的tail节点
}
}
}
}
addWaiter
内部会创建一个保存当前线程的node,并把node挂载到之前的tail后面,让tail重新指向node(代表node是尾节点),最后方法的返回值就是新创建的这个node。会产生一个双向链表,返回Node(Thread-1)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
逻辑
// 参照上图,node就是Node(Thread-1),arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;// 失败标志,默认失败了,看finally块(暂时不重要)
try {
boolean interrupted = false;// 当前是否被打断标志,默认没有
for (;;) {// 死循环
final Node p = node.predecessor();// 返回上一个节点,就是图中的Node(null)
if (p == head && tryAcquire(arg)) {// 如果p是头节点(这里确实是),再次尝试获得锁(刚刚说过,去找NonfairSync中对应的方法),如果获得锁成功
setHead(node);// 获得锁成功,Node(Thread-1)没必要保存了,因为Thread-1不需要阻塞,看下面setHead的源码,就是要将Node(Thread-1)设置成头节点
p.next = null; // help GC 前驱节点也不指向node了,那么这个前驱节点就会被垃圾回收
failed = false;// 没有失败,方式finally中取消获得锁的请求
return interrupted;// 返回中端标记
}
// 如果node的前驱节点p不是头节点,或者尝试获得锁又失败了
if (shouldParkAfterFailedAcquire(p, node) &&// 尝试获得锁失败后应该被暂停吗,看下面,可知第一次循环返回false,就会直接进入下一次循环,如果又发生(node的前驱节点p不是头节点,或者尝试获得锁又失败了),第二次调用shouldParkAfterFailedAcquire返回true
parkAndCheckInterrupt())// 进入真正暂停线程的方法,直到当前线程被打断或者unpark后继续运行
interrupted = true;// 如果parkAndCheckInterrupt返回true,把打断标志记录下来
// 注意,即使线程被打断了,仍然会进入下一次循环尝试获得锁,这也是为什么lock.lock()后调用interrupt不会立即中断的原因
}
} finally {
if (failed)// 如果失败了
cancelAcquire(node);// 就取消当前获得锁的请求
}
}
private void setHead(Node node) {
head = node;// 设置为头节点
node.thread = null;// 把Node(Thread-1)中的线程去掉
node.prev = null;// 不再指向前驱(Node(null))节点
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 前驱节点的状态,最开始进来是0,因为节点的状态还没有修改过
/*
在Node类中规定了4种状态:
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
*/
if (ws == Node.SIGNAL)// 如果是-1,第二次调用就是-1,因为第一次调用的时候已经改成-1了
return true;// 返回应该被暂停
if (ws > 0) {// 如果是1,代表当前线程被取消了
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {// 第一次走这里
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前驱节点的状态从0->-1
}
return false;// 第一次返回false
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 如果当前线程的打断标志为false,暂停当前线程,如果是true则不会阻塞
// 这里当前线程就阻塞住了,从WAITING->RUNNABLE的条件,要么是其他线程调用了当前线程的interrupt方法,要么是调用了LockSupport.unpark(当前线程对象)
return Thread.interrupted();// 返回当前线程是否被打断,如果打断了,同时清除打断标志
}
由此,线程1会进入阻塞状态,那它什么时候会被唤醒呢?(先不考虑interrupt)
对了,就是LockSupport.unpark(Thread-1线程对象)
,该操作其实会在释放锁流程中执行
锁释放原理
public void unlock() {// reentrantLock里面的unlock
sync.release(1);
}
public final boolean release(int arg) {// AQS中的释放流程
if (tryRelease(arg)) {// 同样tryRelease也是子类需要实现的,找NonfairSync或者Sync
// 如果锁释放成功
Node h = head;
if (h != null && h.waitStatus != 0)// 头节点不是空,且头节点的状态不是0,之所以要加!=null的校验,是因为如果h是空,证明没有发生锁竞争,就是当前线程在获得锁到释放锁期间没有其他线程调用acquire,自然就没必要去唤醒处于阻塞的线程(没有嘛)。h.waitStatus != 0为什么加后面讲
unparkSuccessor(h);// 重新唤醒阻塞中的线程
return true;// 释放锁成功
}
return false;// 释放锁失败
}
看Sync类(NonfairSync的直接父类)中的tryRelease(int)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// state - 1,表示锁重入计数-1,因为一个线程获取n次锁,state=n
if (Thread.currentThread() != getExclusiveOwnerThread())// 如果当前锁是A线程持有的,结果B线程来释放锁,必然不合理,抛出异常
throw new IllegalMonitorStateException();
boolean free = false;// 当前锁是否空闲
if (c == 0) {// 如果锁重入计数=0,就代表当前线程可以将真正锁了
free = true;// 锁释放成功了
setExclusiveOwnerThread(null);// owner会在下一次竞争重新被赋值
}
setState(c);
return free;
}
可见tryRelease中,判断是否释放锁成功,就是看锁重入计数(state)是不是为0了,为0就成功释放,不为0,证明当前线程加了n次锁,只释放了不到n次,那这把锁还是应该属于当前线程的,所以锁释放失败。
下面考虑,锁真正释放后,会执行的unparkSuccessor(h)
private void unparkSuccessor(Node node) {// node这里的实参就是头节点
int ws = node.waitStatus;// 头节点的状态,不知道大家还记不记得在Thread-1 park前,曾经获得过它的前驱节点,并在shouldParkAfterFailedAcquire方法中将其值设为了-1
if (ws < 0)// 这里确实<0
compareAndSetWaitStatus(node, ws, 0);// 将头节点状态改成0,代表我要开始唤醒处于阻塞的线程了,现在解释一下为什么要在release中加h.waitStatus != 0校验,就是为了放置重复唤醒
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)// 如果头节点的下一个节点不是null,
LockSupport.unpark(s.thread);// 唤醒Thread-1
}
线程1被唤醒后,应该执行
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 正常unpark
return Thread.interrupted();// false
}
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)) {// 这里又去尝试获得锁,但是这里还是可能会失败,确实unparkSuccessor方法只唤醒了一个线程(就是当前这个线程),挂载在Node(Thread-1)后面的线程都不会被唤醒,但是可能会有外部线程调用lock.lock(),但大概率是会成功的
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// 返回false,进入下一次循环
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
思考如下问题
Lock lock = new ReentrantLock();// 创建的锁默认是公平还是非公的?
new Thread(() -> {
lock.lock();
Thread.sleep(1000000);// catch异常省略
}, "t1").start();
Thread.sleep(1000);
lock.lock();// 解释这里为什么会阻塞住,底层用的是什么方式?
System.out.println("成功");
Lock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
Thread.sleep(10000);// catch异常省略
// 释放锁
}, "t1").start();
Thread t2 = new Thread(() -> {
Thread.sleep(1000);
lock.lock();// 这里会阻塞
System.out.println("t2后续代码");
// 请问,如果t1在10秒后释放锁后,下面这个语句会不会输出,如果会输出,输出什么,为什么?
System.out.println(Thread.currrentThread().isInterrupted());
}, "t2");
t2.start();
Thread.sleep(2000);// 主线线程2秒后
t2.interrupt();// 请问这里打断t2,会不会立即输出"t2后续代码",为什么?
System.out.println("成功");