前言
本篇博客是《Java锁深入理解》系列博客的第四篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结
Java锁深入理解2——ReentrantLock
Java锁深入理解3——synchronized
Java锁深入理解4——ReentrantLock VS synchronized
Java锁深入理解5——共享锁
怎么选择
一般情况下我们选择synchronized即可。
- 虽说ReentrantLock更灵活。但synchronized首先写起来就很方便。
- 而且synchronized通过优化的锁升级机制,性能更高。synchronized相当于无锁+偏向锁+轻量级锁+ReentrantLock。
- 无论是开源框架还是JDK自己的代码,synchronized的应用都比ReentrantLock更普遍。
如ConcurrentHashMap在1.8版就每个segment的分段锁ReentrantLock删掉了,改为使用CAS+synchronized。
再比如,相比较用ReentrantLockd的logback。性能更好的log4j2也是用的synchronized(当然,log4j2性能比较好,还有很多其他优化的地方。比如用性能更强悍的环形队列)
中断
在说到ReentrantLock 和 synchronized区别时,会提到一点:ReentrantLock可以被中断,synchronized不可被中断。
你是否想过这句话是准确的含义是什么。
怎么中断
是通过报错吗?并不是。这里的中断特指一种给线程发的信号(注意:这仅仅是一种信号。至于怎么处理这种信号,需要有其他逻辑去实现。有点类似Java“注解”的概念),让线程停下来。
怎么发信号呢
t.interrupt();//t是一个线程
这样线程就接收到了中断信号。但是线程本身是不会处理信号的,它只负责接收。处理信号,需要t线程内部去处理。比如sleep, wait, ReentrantLock。(如果对中断没有概念的,可以先看看我的另一篇博客,其中有中断的介绍,这里就不再重复了)
所以,前面那句话的意思就是:如果线程中使用了ReentrantLock,它会响应中断信号,并报一个异常。 而synchronized却会无视这种信号。
说到这里,你也许对如何响应中断还是没个清晰的概念。那就做个实验。写个直观的demo看一下。
怎么写demo呢。你可能会想,写一个线程,里面套上ReentrantLock,然后在里面sleep几秒钟,启动后,在主线程执行中断信号t.interrupt();
就行了。但如果你看过前面那篇博客,就会知道sleep本身就会响应中断信号而报错(wait也是)。这不就是在引入干扰因素了吗。
其实上面的想法,一开始就错了。因为ReentrantLock响应中断,不是在运行中,而是在等待中。
而且要响应中断,我们不能使用普通的.lock()方法,而是使用lockInterruptibly()方法来抢锁。
所以完整的说法是:如果线程中使用了ReentrantLock的lockInterruptibly(),线程在等待过程中会响应中断信号,并报一个异常。 而synchronized在等待过程中却会无视这种信号。
这样说的话,demo好像更难写了。我们怎么抓住稍纵即逝的“抢锁等待间隙”呢。换个思路,我们可以让一个线程“望眼欲穿的一直等”呀。
也就是让另一个线程走在它前面,通过ReentrantLock同步挡着他,不让它走(ReentrantLock不就是干这个事的吗)。
Demo
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
/**
* 在这个示例中,我们创建了两个线程,都试图获取同一个ReentrantLock实例。
* 我们使用`lockInterruptibly()`方法获取锁,这样当一个线程等待锁时,可以响应中断。
*
* `main()`方法中,我们启动两个线程,让它们尝试获取锁。
* 然后,我们等待1秒,以确保两个线程都开始运行。接着,我们中断第二个线程,使用`thread2.interrupt()`方法。
* 当第二个线程被中断时,`lockInterruptibly()`方法抛出InterruptedException异常,
* 我们在异常处理代码中输出线程被中断的信息,并重新设置线程的中断状态。
*
* 最后,我们等待两个线程执行完毕,示例结束。
*/
public class ReentrantLockInterruptExample {
private final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ReentrantLockInterruptExample example = new ReentrantLockInterruptExample();
// 创建并启动两个线程
Thread thread1 = new Thread(() -> example.method(), "Thread 1");
Thread thread2 = new Thread(() -> example.method(), "Thread 2");
thread1.start();
TimeUnit.MILLISECONDS.sleep(1);//保证线程1先抢到锁,然后把线程2堵住
thread2.start();
// 等待两个线程都开始运行
TimeUnit.SECONDS.sleep(1);
// 中断第二个线程
thread2.interrupt();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
}
/**
* 证明了:lockInterruptibly()可以在等待的时候被打断
*/
public void method() {
try {
System.out.println(Thread.currentThread().getName() + " trying to acquire the lock...");
lock.lockInterruptibly(); // 可以响应中断的锁获取方法(替换原来动lock.lock()方法)
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
// 临界区代码
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "--------------2-");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
} catch (InterruptedException e) {
// 处理中断逻辑
System.out.println(Thread.currentThread().getName() + " was interrupted.");
Thread.currentThread().interrupt(); // 重新设置中断状态
}
}
}
运行结果:
Thread 1 trying to acquire the lock...
Thread 1 acquired the lock.
Thread 2 trying to acquire the lock...
Thread 2 was interrupted.
Thread 1--------------2-
Thread 1 released the lock.
Thread 2没能抢到锁,接收到中断信号之后,直接报错进入catch。
注意:Thread 1是不会响应异常的,因为thread2.interrupt();
是专门针对Thread 2发的中断信号。
同样,你可以把lockInterruptibly()换回原来的lock(),看看会不会报错,对照一下。
而如果把ReentrantLock换成synchronized,却发现synchronized根本没有这样的检查异常可以被捕获,所以还得把catch删掉。
最终我们验证了上面的结论:ReentrantLock可以响应异常,而synchronized不行。
会在运行中被中断吗
lockInterruptibly()可以在等待的时候被打断。那么在运行时会被打断吗?
答:不会。看过AQS源码,我们知道:锁根本不关心程序动运行过程。它只会在线程碰到lock.lock(), conditon.await()等节点时起作用。
线程运行过程中和锁没半毛钱关系,锁并不会去监视线程的运行。
就像高速公路动收费站,只会在开始结尾时管你,中间并不会管你干什么。
而"等待中"之所以可以被中断,那是因为"等待中"并不是属于运行时。相当于你还堵在收费站门口。所以它才可以中断你。
那么synchronized也是一样,只不过它在等待过程中也不管你。
什么机制
直接原因就是lockInterruptibly()
代码中有:
使用Thread.interrupted()
判断接受到中断信号的时候,就抛出异常throw new InterruptedException();
我在之前那篇博客中介绍中断的用法,demo的写法就是这样:自己发信号,自己接受信号,自己处理。很常规的思路。
但是,在lockInterruptibly()
有两处判断并抛异常的地方,一个是一开始抢锁的时候,就是上面所说的常规处理中断信号的方式。
还有一个是循环抢锁的过程中。
当这里返回true,就会执行if语句方法体内的抛中断异常语句了。
但这个方法为什么会返回true呢,之前我们在看普通lock()方法时也是这样的逻辑,为什么那个地方不会返回true呢。这就是parkAndCheckInterrupt这个方法的神奇之处了。
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这是AQS中的一个方法:阻塞并检查中断。
为什么这个方法要单独讲讲,因为我们前面一直说阻塞(park)。但从未见过庐山真面目。它就长这样。而且这个方法有它的神奇之处。
会停下来,我们已经见识过了。然后接收到unpark信号之后,会继续走,我们也知道。但它重新被唤醒,除了因为unpark,还有一种情况:收到t.interrupted();
中断信号。
之前我们可能一直认为:中断(interrupted),它只是一个信号。
没想到在这里,它真的能直接对线程的动作起作用(当然,再往下看c++代码,肯定也仅仅是接收信号,然后让线程继续走。
只是说,站在Java代码层面,中断信号在这里的表现不仅仅是一个“信号”那么简单,它将和unpark属于一个级别)。
所以说:park让线程停下来,而unpark和interrupted会让线程继续运行。
但看过代码我们就知道,前面我们在说await/signal,本质就是park/unpark在起作用(一个负责停,一个负责重新启动)
而这里我们说中断。park负责停,而interrupted负责重新启动。
关键的来了:上面这两种情况居然用的是同一套代码,就是上面我展示这个parkAndCheckInterrupt方法。
这不会出现混乱吗?都是唤醒线程,我们哪知道它是怎么被唤醒的。
就是方法中那个我们可能一直忽略的Thread.interrupted()
在起作用。
Thread.interrupted():
这个方法在说中断的时候也讲过,意思就是判断:线程有没有接收到中断信号,有接收到返回true,没有就返回false(和t.interrupted()配合使用,里应外合对线程逻辑做小动作)。其实还没完。信号相当于写在小黑板上的一个标记。当外部给线程发中断信号,就是往小黑板上写了一个中断标记。然后就可以被
Thread.interrupted()
读到,读完之后:它就把这个标记擦掉了。(有点像“阅后即焚”)。再去读(执行Thread.interrupted()
)就读不到了。
所以,相当于Thread.interrupted()
有两个功能,先阅后焚。
接着讲parkAndCheckInterrupt方法。
有了Thread.interrupted(),我们就能弥补线程莫名其妙被唤醒的尴尬:当线程再次开始执行,就执行Thread.interrupted(),它的返回值将会告我们,它是被什么唤醒的。
现在就很完美了。这也是park的官方推荐的严谨使用方法。
小结
- ReentrantLock可以响应异常,而synchronized不行
- ReentrantLock响应中断是指 线程等待时中断,不会在运行中中断(计算机不存在什么无所不在的“上帝”,“超距感应”。其实底层原理都特别简单。)
- 中断的本质还是中断的常规用法:t.interrupted()和Thread.interrupted()配合使用,里应外合传信号,手动处理逻辑。
- 中断除了可以作为单纯的信号,还可以直接作用在阻塞park上,起到唤醒阻塞的作用。可以通过Thread.interrupted()来区分是不是因为中断而苏醒的。