ReentrantLock
在了解ReentrantLock之前,我们首先回忆一下synchronized,synchronized是java内置的关键字,锁的获取和释放都是由jvm实现,因此用户就不需要显示的去释放锁,是一种独占的加锁方式,但是虽然方便,也有一定的弊端:
- 1.当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
- 2.如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待
接下来我们还需要了解几个相关的概念:
- 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁。
- 可中断锁:可中断锁时只线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的。
- 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁
1.ReentrantLock的使用方式
public class Demo {
ReentrantLock lock = new ReentrantLock();
public static int num = 0;
public void add(){
lock.lock();
try{
num++;
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread t1 = new Thread(()->{
for(int i=0;i<1000;i++){
demo.add();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<1000;i++){
demo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo.num);
}
}
使用过程:
- 创建锁:ReentrantLock lock = new ReentrantLock();
- 获取锁:lock.lock()
- 释放锁:lock.unlock();
注意释放锁要放在finally代码块中,这样能保证无论如何线程都会释放锁。
2.ReentrantLock是可重入锁
public class Demo {
ReentrantLock lock = new ReentrantLock();
public static int num = 0;
public void add(){
lock.lock();
lock.lock();
try{
num++;
}finally {
lock.unlock();
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread t1 = new Thread(()->{
for(int i=0;i<1000;i++){
demo.add();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<1000;i++){
demo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo.num);
}
}
在这里lock.lock();
执行了两遍,第一次执行获取到了锁,第二次同样可以执行,所以一个线程可以多次获取同一把锁,说明ReentrantLock是可重入锁,不过在这里需要注意,获取了多少次锁,就要释放多少次锁,否则,该线程还是一直占有着该把锁,其它线程仍然会一直阻塞。因此lock()
和unlock()
一定要是成对出现的。
2.ReentrantLock与公平锁
大多数情况下都是非公平锁,ReentrantLock的默认构造方法是非公平锁。如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是先来的就先获取到锁。
根据源码可以知道,构造方法时加上一个true就可以创建公平锁
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁测试:
public class FairLock {
ReentrantLock lock = new ReentrantLock(true);
public void add(String str){
lock.lock();
try{
System.out.println(str);
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
FairLock fairlock = new FairLock();
Thread t1 = new Thread(()->{
for(int i=0;i<3;i++){
fairlock.add("T1获取到锁");
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<3;i++){
fairlock.add("T2获取到锁");
}
});
Thread t3 = new Thread(()->{
for(int i=0;i<3;i++){
fairlock.add("T3获取到锁");
}
});
Thread t4 = new Thread(()->{
for(int i=0;i<3;i++){
fairlock.add("T4获取到锁");
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果:
T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁
T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁
T2获取到锁
T1获取到锁
T3获取到锁
T4获取到锁
可以看到获取锁的顺序都是:2,1,3,4,说明锁时按照先后顺序获得的。
如果改成非公平锁:把true
改为false
;
那么结果如下
T1获取到锁
T3获取到锁
T3获取到锁
T3获取到锁
T2获取到锁
T2获取到锁
T2获取到锁
T1获取到锁
T1获取到锁
T4获取到锁
T4获取到锁
T4获取到锁
可以看到t3可能会连续获得锁,结果是比较随机的,不公平的。为什么会出现线程连续获得锁的情况呢?当一个线程请求锁时,只要获取了同步状态即成功获取锁,在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。
4.ReentrantLock获取锁的过程是可中断的
对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:
- 1.要么获取到锁然后继续后面的操作
- 2.要么一直等待,直到其他线程释放锁为止
而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小时朋友还没到,突然你接到一个电话,朋友由于突发状况,不能来了,那么你一定达到回府。中断操作正是提供了一套类似的机制,如果一个线程正在等待获取锁,那么它依然可以收到一个通知,被告知无需等待,可以停止工作了。
public class InterruptLockDemo extends Thread{
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
private int threadNum;
private String name;
public InterruptLockDemo(String name,int threadNum){
super(name);
this.name = name;
this.threadNum = threadNum;
}
@Override
public void run() {
try{
if(threadNum==1){
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
}
else{
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
}catch (InterruptedException e){
System.out.println("线程"+name+"被中断,中断标志位:"+this.isInterrupted());
e.printStackTrace();
}finally {
// 如果lock1被当前线程拥有,就释放
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
if(lock2.isHeldByCurrentThread()){
lock2.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new InterruptLockDemo("t1",1);
Thread t2 = new InterruptLockDemo("t2",2);
t1.start();
t2.start();
}
运行如上代码会产生死锁,lock1被线程t1占用,lock2倍线程t2占用,线程t1在等待获取lock2,线程t2在等待获取lock1,都在相互等待获取对方持有的锁,最终产生了死锁,如果是在synchronized关键字情况下发生了死锁现象,程序是无法结束的。使用jps和jstack,可以查看到如下信息:
Found one Java-level deadlock:
=============================
"t2":
waiting for ownable synchronizer 0x000000076b424700, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t1"
"t1":
waiting for ownable synchronizer 0x000000076b424730, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t2"
我们对上面代码改造一下,线程t2一直无法获取到lock1,那么等待5秒之后,我们中断获取锁的操作。主要修改一下main方法,如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new InterruptLockDemo("t1",1);
Thread t2 = new InterruptLockDemo("t2",2);
t1.start();
t2.start();
// 等待5秒后中断t2线程
TimeUnit.SECONDS.sleep(5);
t2.interrupt();
}
线程t2响应中断请求,然后释放锁
线程t2被中断,中断标志位:false
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at ReentrantLock.InterruptLockDemo.run(InterruptLockDemo.java:33)
为什么中断标志位是false?t2线程调用了interrupt()方法,将线程的中断标志置为true,线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。因此中断标志位变化是:false -> true -> false
- 关于获取锁的过程中被中断,注意几点:
- ReentrankLock中必须使用实例方法lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
- 线程调用interrupt()之后,线程的中断标志会被置为true
- 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
- 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false
5.lock,tryLock,lockInterruptibly区别
- lock:拿不到锁就会一直阻塞。
- tryLock:尝试获取锁,如果获取到了锁,就返回true,没有拿到就返回false。如果是带有时间的tryLock,那么没有拿到锁,就先等一段时候,超时之后还未拿到锁,就返回false。
- lockInterruptibly:lockInterruptibly和lock一样,没有拿到锁,就被阻塞,但是如果此时有interrupt,则“此线程会被唤醒并被要求处理InterruptedException”,并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException
获取锁的4种方法对比:
总结
- ReentrantLock可以实现公平锁和非公平锁
- ReentrantLock默认实现的是非公平锁
- ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
释放锁的操作必须放在finally中执行 - lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发 InterruptedException异常
- 关于 InterruptedException异常说一下,看到方法声明上带有 throwsInterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发 InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用 interrupt()方法而触发 InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
- 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
- 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
附上ReentrantLock源码——获取公平锁、非公平锁、释放锁:
https://www.jianshu.com/p/259d076ada14
原文链接:
https://blog.csdn.net/u013851082/article/details/70140223
https://mp.weixin.qq.com/s/gm-EQp_dP7fKHe4-AzLX0g