ReentrantLock源码解析(补充2——可打断的锁)
上一章 ReentrantLock源码解析 仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:
- AQS 中阻塞的线程被唤醒后的执行流程
- 可打断的锁
lock.lockInterruptibly()
(本篇讲述) - 锁超时
lock.tryLock(long,TimeUnit)
- 条件变量 Condition
1. 可打断锁的使用
打断锁的使用场景:线程 A 持有锁,并且临界区执行时间很长,线程 B 不想等待那么久,可以被中断,并放弃锁的争抢(退出 AQS 的等待队列)。
场景模拟如下:
- 线程 t2 中临界区执行时间很长。
- 线程 t1 如果等待超过5秒还是等不到锁资源,就中断t1的等待。
public class ReentrantLockDemo1 {
ReentrantLock lock = new ReentrantLock();
Runnable r_locked = new Runnable() {
@Override
public void run() {
lock.lock();
try{
//模拟临界区的执行时间很长
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Runnable r = new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+" 尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+" 还没获取到锁,被中断了");
return;
}
try{
//临界区代码
System.out.println(Thread.currentThread().getName()+" 获取到锁,并执行临界区代码");
}finally {
System.out.println(Thread.currentThread().getName()+" 释放锁");
lock.unlock();
}
}
};
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo1 demo = new ReentrantLockDemo1();
//t1等待锁
Thread t1 = new Thread(demo.r,"t1");
//t2中临界区执行时间很长
Thread t2 = new Thread(demo.r_locked,"t2");
t2.start();
t1.start();
//我希望,如果五秒后,t1 还是等不到锁的释放,那么中断t1,让它退出锁的等待
TimeUnit.SECONDS.sleep(3);
System.out.println("打断t1的等待");
t1.interrupt();
}
}
运行结果如下:
t1 尝试获取锁
打断t1的等待
t1 还没获取到锁,被中断了
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)...
Process finished with exit code 0
可以发现,线程 t2 还在持有锁资源, 我们不希望线程 t1 继续等待锁资源,中断线程 t1, t1 响应中断,并抛出异常。
如果 t1 使用的是 lock.lock() 则 t1 在等待过程中,不会响应中断。
这里所谓的响应中断: 如果由于 interrupt() 导致线程从 等待状态(WAITING) 回到运行态 (RUNNABLE),该线程的中断标志为 true, 可以通过判断中断标志,并抛出异常,来响应中断。这部分原理在下面源码分析有解释。
2. 可打断原理
通过 lock.lockInterruptibly()
来进入可打断的锁获取。
@Override
public void run(){
try{
lock.lockInterruptibly();
}catch(InterruptedException e){
//处理等待锁时,线程被中断后的处理
return;
}
//如果没被中断,且等到并获取到了锁,进入临界区
try{
//临界区代码
}finally{
//释放锁
lock.unlock();
}
}
ReentrantLock 调用 sync.acquireInterruptibly(1) 来启动可打断锁,它和 AQS#acquire() 一样是一个模板方法。
//sync 调用的是父类 AQS 的acquireInterruptibly()方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果当前线程本来就被中断,就响应中断,不进行锁的获取
if (Thread.interrupted())
throw new InterruptedException();
//尝试锁的获取——具体实现分公平锁与非公平锁
//公平锁:先看队列有没有等待节点,如果没有才cas一次尝试获取锁
//非公平锁:先cas一次尝试获取锁
if (!tryAcquire(arg))
//如果没获取到锁,准备进入等待队列
//doAcquireInterruptibly()实现内容 与 lock()方法调用链中的 accquireQueued(addWaiter(),1) 几乎一样
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly() 就是在原先 acquireQueued() 的 for( ; ; ) 基础上更改为可响应中断(可以抛出中断异常):
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//把 addWaiter() 方法放到里面来实现
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//在 park 过程中如果被 interrupt 会进入此
// 这时候抛出异常,而不会再次进入 for(;;)
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//LockSupport.park()使Java线程从运行态(RUNNABLE)进入到 等待状态(WAITING)
//LockSupport.unpark() 或者 thread.interrupt()方法都可以让Java线程从 等待状态回到运行态。后者多了一步:将线程的中断标记置为 true
return Thread.interrupted();
}
对比看一下 acquireQueued() 方法,只有下面展示的一处不同:
final boolean acquireQueued(final Node node, int arg) {
try {
for (;;) {
。。。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//只有这里不同!!!
interrupted = true;
}
} finally {
。。。
}
}
显然,两个方法都是死循环。
不同的是,doAcquireInterruptibly() 在死循环中,如果发现线程被 interrupt() 将会抛出异常,抛出异常后不再进入 for( ; ; ) :
parkAndCheckInterrupt() 由于 interrupt() 方法,继续执行 return方法,返回了线程的中断标记,由于 interrupt() 将线程的中断标记设为 true,所以在死循环中,会进入if方法体,执行 InterruptedException
异常的抛出。抛出异常后不再进入 for ( ; ; ) 。
而 acquireQueued() 则不会因为线程被 interrupt() 而抛出异常,而是继续进入for( ; ; ) :
acquireQueued() 中,线程如果被 interrupt() ,parkAndCheckInterrupt() 确实会返回 true, 但仅仅将 interrupted 局部变量设为 true,然后直接再次进入循环。由于此时锁资源还在被使用,该线程还会进入等待状态: parkAndCheckInterrupt() 方法并通过 LockSupport.park() 使该线程又进入AQS的等待队列并阻塞等待。