目录
在上篇Java并发编程:Lock接口中,我们了解到Lock类有一个唯一的实现类ReentrantLock类。
ReentrantLock重入锁:支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入(可见Java并发编程:线程安全&synchronized关键字)。同时,ReentrantLock还支持公平锁和非公平锁两种方式。
一、ReentrantLock的重入性
重入性可以理解为锁的分配机制:基于线程的分配,而不是基于方法调用的分配。我们可以用synchronized关键字来理解:
class Test {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生这种状况。
二、公平锁与非公平锁
ReentrantLock支持两种锁:公平锁和非公平锁
- 公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁。
- 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
这么一看synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
ReentrantLock的构造方法无参时是构造非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁与非公平锁比较:
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
另外在ReentrantLock类中定义了很多方法,比如:
- isFair() //判断锁是否是公平锁
- isLocked() //判断锁是否被任何线程获取了
- isHeldByCurrentThread() //判断锁是否被当前线程获取了
- hasQueuedThreads() //判断是否有线程在等待该锁
在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。
三、测试代码
1、中断响应
对于synchronized块来说,要么获取到锁执行,要么持续等待。而重入锁的中断响应功能就合理地避免了这样的情况。比如,一个正在等待获取锁的线程被“告知”无须继续等待下去,就可以停止工作了。下面代码演示了使用重入锁来解决死锁:
先使用lock方法上锁
import java.util.concurrent.locks.ReentrantLock;
public class KillDeadlock implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lockRule;
public KillDeadlock(int lockRule) {
this.lockRule = lockRule;
}
public void run() {
try {
if (lockRule == 1) {
lock1.lock(); //通过lock方法给lock1上锁
if(lock1.isLocked()){ //判断lock1是否上锁了
System.out.println(Thread.currentThread().getName()+"'s lock1 is locked");
}
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+" After 0.5s");
} catch (InterruptedException e) {}
lock2.lock();
if(lock2.isLocked()){
System.out.println(Thread.currentThread().getName()+"'s lock2 is locked");
}
} else {
lock2.lock();
if(lock2.isLocked()){
System.out.println(Thread.currentThread().getName()+"'s lock2 is locked");
}
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName()+ "After 0.5s");
} catch (InterruptedException e) {}
lock1.lock();
if(lock1.isLocked()){
System.out.println(Thread.currentThread().getName()+"'s lock1 is locked");
}
}
}finally {
if (lock1.isHeldByCurrentThread()) { //判断lock1是否持有线程
lock1.unlock();
if(!lock1.isLocked()){
System.out.println(Thread.currentThread().getName()+"'s lock1 is unlocked");
}
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
if(!lock2.isLocked()){
System.out.println(Thread.currentThread().getName()+"'s lock2 is unlocked");
}
}
System.err.println(Thread.currentThread().getName() + "结束!");
}
}
public static void main(String[] args) throws InterruptedException {
KillDeadlock deadLock1 = new KillDeadlock(1);
KillDeadlock deadLock2 = new KillDeadlock(2);
Thread t1 = new Thread(deadLock1);
t1.setName("t1");
Thread t2 = new Thread(deadLock2);
t2.setName("t2");
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("After 1s");
t2.interrupt();
}
}
运行结果:
t1、t2线程开始运行时,会分别持有lock1和lock2而请求去lock1和lock2,这样就发生了死锁,t1和t2一直处于等待状态。由于通过lock上锁不能被interrupt
而此时我们通过lockInterruptibly方法上锁时,结果如下:
可见t2.interrup时即t2线程状态标记为中断后,持有重入锁lock2的线程t2会响应中断,并不再继续等待lock1,同时释放了其原本持有的lock2,这样t1获取到了lock2,正常执行完成。t2也会退出,但只是释放了资源并没有完成工作。
2、锁申请等待限时
tryLock()或者tryLock(long timeout, TimeUtil unit) 方法进行一次限时的锁等待。
- tryLock():当线程尝试获取锁时,如果获取到锁则继续执行,如果锁被其他线程持有,则立即返回 false ,也就是不会使当前线程等待,所以不会产生死锁
- tryLock(long timeout, TimeUtil unit) :在指定时长内获取到锁则继续执行,如果等待指定时长后还没有获取到锁则返回false
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockTest implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public void run() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 在1秒内尝试上锁
System.out.println(Thread.currentThread().getName()+"'s lock is locked");
Thread.sleep(2000); //休眠2秒
} else {
System.err.println(Thread.currentThread().getName() + "获取锁失败!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
TryLockTest test = new TryLockTest();
Thread t1 = new Thread(test);
t1.setName("t1");
Thread t2 = new Thread(test);
t2.setName("t2");
t1.start();
t2.start();
}
}
t1先获取到锁,并休眠2秒,这时t2开始尝试上锁,在1秒后依然没有获取到锁,就不再继续等待。
3、公平锁
公平锁,就是按照时间先后顺序,使先等待的线程先得到锁,而且,公平锁不会产生饥饿锁,也就是只要排队等待,最终能等待到获取锁的机会。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockTest implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
public void run() {
while (true) {
try {
lock.lock();
System.err.println(Thread.currentThread().getName() + "获取到了锁!");
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
FairLockTest test = new FairLockTest();
Thread t1 = new Thread(test, "t1");
Thread t2 = new Thread(test, "t2");
t1.start();t2.start();
}
}
部分运行结果
可见t1和t2交替获取到锁,如果是非公平锁的话就不一定了
参考资料
1、https://juejin.im/post/5aeb0a8b518825673a2066f0
2、https://blog.csdn.net/weixin_39910081/article/details/80147754