ReentrantLock
ReentrantLock 是什么?
ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
public class ReentrantLock implements Lock, java.io.Serializable {}ReentrantLock 里面有一个内部类 Sync,Sync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。
// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}从上面的内容可以看出, ReentrantLock 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 AQS 详解 这篇文章。
公平锁和非公平锁有什么区别?
- 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
⭐️synchronized 和 ReentrantLock 有什么区别?
两者都是可重入锁
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。
JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。
在下面的代码中,method1() 和 method2()都被 synchronized 关键字修饰,method1()调用了method2()。
public class SynchronizedDemo {
public synchronized void method1() {
System.out.println("方法1");
method2();
}
public synchronized void method2() {
System.out.println("方法2");
}
}由于 synchronized锁是可重入的,同一个线程在调用method1() 时可以直接获得当前对象的锁,执行 method2() 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如synchronized是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 method2()时获取锁失败,会出现死锁问题。
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:
- 等待可中断 :
ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说当前线程在等待获取锁的过程中,如果其他线程中断当前线程「interrupt()」,当前线程就会抛出InterruptedException异常,可以捕捉该异常进行相应处理。 - 可实现公平锁 :
ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。 - 通知机制更强大:
ReentrantLock通过绑定多个Condition对象,可以实现分组唤醒和选择性通知。这解决了synchronized只能随机唤醒或全部唤醒的效率问题,为复杂的线程协作场景提供了强大的支持。 - 支持超时 :
ReentrantLock提供了tryLock(timeout)的方法,可以指定等待获取锁的最长等待时间,如果超过了等待时间,就会获取锁失败,不会一直等待。
如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。
关于 Condition接口的补充:
Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。
关于 等待可中断 的补充:
lockInterruptibly()会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待。在阻塞等待的过程中,如果其他线程中断当前线程
interrupt(),就会抛出InterruptedException异常,可以捕获该异常,做一些处理操作。为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解
lockInterruptibly()可以响应中断:public class MyRentrantlock { Thread t = new Thread() { @Override public void run() { ReentrantLock r = new ReentrantLock(); // 1.1、第一次尝试获取锁,可以获取成功 r.lock(); // 1.2、此时锁的重入次数为 1 System.out.println("lock() : lock count :" + r.getHoldCount()); // 2、中断当前线程,通过 Thread.currentThread().isInterrupted() 可以看到当前线程的中断状态为 true interrupt(); System.out.println("Current thread is intrupted"); // 3.1、尝试获取锁,可以成功获取 r.tryLock(); // 3.2、此时锁的重入次数为 2 System.out.println("tryLock() on intrupted thread lock count :" + r.getHoldCount()); try { // 4、打印线程的中断状态为 true,那么调用 lockInterruptibly() 方法就会抛出 InterruptedException 异常 System.out.println("Current Thread isInterrupted:" + Thread.currentThread().isInterrupted()); r.lockInterruptibly(); System.out.println("lockInterruptibly() --NOt executable statement" + r.getHoldCount()); } catch (InterruptedException e) { r.lock(); System.out.println("Error"); } finally { r.unlock(); } // 5、打印锁的重入次数,可以发现 lockInterruptibly() 方法并没有成功获取到锁 System.out.println("lockInterruptibly() not able to Acqurie lock: lock count :" + r.getHoldCount()); r.unlock(); System.out.println("lock count :" + r.getHoldCount()); r.unlock(); System.out.println("lock count :" + r.getHoldCount()); } }; public static void main(String str[]) { MyRentrantlock m = new MyRentrantlock(); m.t.start(); } }输出:
lock() : lock count :1 Current thread is intrupted tryLock() on intrupted thread lock count :2 Current Thread isInterrupted:true Error lockInterruptibly() not able to Acqurie lock: lock count :2 lock count :1 lock count :0
关于 支持超时 的补充:
为什么需要
tryLock(timeout)这个功能呢?
tryLock(timeout)方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回true;如果在锁可用之前超时,则返回false。此功能在以下几种场景中非常有用:
- 防止死锁: 在复杂的锁场景中,
tryLock(timeout)可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。- 提高响应速度: 防止线程无限期阻塞。
- 处理时间敏感的操作: 对于具有严格时间限制的操作,
tryLock(timeout)允许线程在无法及时获取锁时继续执行替代操作。
可中断锁和不可中断锁有什么区别?
它们的区别在于:线程在获取锁的过程中被阻塞时,是否能够因为中断而提前放弃等待。
- 不可中断锁:线程在等待锁期间即使收到中断信号,也不会退出阻塞状态,而是一直等待直到获得锁。中断状态会被保留,但不会影响锁的获取过程。
synchronized属于典型的不可中断锁。ReentrantLock#lock()也是不可中断的。
- 可中断锁:线程在等待锁的过程中如果收到中断信号,会立即停止等待并抛出
InterruptedException,从而有机会进行取消或错误处理。ReentrantLock#lockInterruptibly()实现了可中断锁。ReentrantLock#tryLock(long time, TimeUnit unit)(带超时的尝试获取)也是可中断的。
评论
使用 GitHub 账号即可参与加载较慢?可 直接前往 GitHub Discussions 查看与参与。