显示锁是什么?
显式锁就是程序员通过 Lock 接口手动 lock() 加锁、手动 unlock() 解锁的锁。它和 synchronized 这种“隐式锁”相对
Lock 和 Condition
为何引入 Lock 和 Condition
并发编程领域两大核心问题:互斥和同步。这两大问题,管程(Monitor)都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
管程是一个理论模型,而synchronized编译之后的monitor就是这个理论的实现。这里的Lock和Condition也是。
既然 synchronized 是管程的一种实现,何必再提供 Lock 和 Condition。
JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。但是,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会出现死锁。
synchronized 无法通过破坏不可抢占条件来避免死锁。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
与内置锁 synchronized 不同的是,Lock 提供了一组无条件的、可轮询的、定时的以及可中断的锁操作,所有获取锁、释放锁的操作都是显式的操作。
- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
Lock 接口
Lock 的接口定义如下:
1 | public interface Lock { |
lock()- 获取锁。unlock()- 释放锁。tryLock()- 尝试获取锁,仅在调用时锁未被另一个线程持有的情况下,才获取该锁。tryLock(long time, TimeUnit unit)- 和tryLock()类似,区别仅在于限定时间,如果限定时间内未获取到锁,视为失败。lockInterruptibly()- 锁未被另一个线程持有,且线程没有被中断的情况下,才能获取锁。newCondition()- 返回一个绑定到Lock对象上的Condition实例。
Condition
Condition 实现了管程模型里面的条件变量。
在单线程中,一段代码的执行可能依赖于某个状态,如果不满足状态条件,代码就不会被执行(典型的场景,如:if ... else ...)。在并发环境中,当一个线程判断某个状态条件时,其状态可能是由于其他线程的操作而改变,这时就需要有一定的协调机制来确保在同一时刻,数据只能被一个线程锁修改,且修改的数据状态被所有线程所感知。
Java 1.5 之前,主要是利用 Object 类中的 wait、notify、notifyAll 配合 synchronized 来进行线程间通信。
wait、notify、notifyAll 需要配合 synchronized 使用,不适用于 Lock。而使用 Lock 的线程,彼此间通信应该使用 Condition 。这可以理解为,什么样的锁配什么样的钥匙。内置锁(synchronized)配合内置条件队列(wait、notify、notifyAll ),显式锁(Lock)配合显式条件队列(Condition )。
Condition 的特性
Condition 接口定义如下:
1 | public interface Condition { |
其中,await、signal、signalAll 与 wait、notify、notifyAll 相对应,功能也相似。除此以外,Condition 相比内置条件队列( wait、notify、notifyAll ),提供了更为丰富的功能:
- 每个锁(
Lock)上可以存在多个Condition,这意味着锁的状态条件可以有多个。 - 支持公平的或非公平的队列操作。
- 支持可中断的条件等待,相关方法:
awaitUninterruptibly()。 - 支持可定时的等待,相关方法:
awaitNanos(long)、await(long, TimeUnit)、awaitUntil(Date)。
Condition 的用法
这里以 Condition 来实现一个消费者、生产者模式。
产品类
1 | class Message { |
消费者
1 | class MessageConsumer implements Runnable { |
生产者
1 | class MessageProducer implements Runnable { |
测试
1 | public class LockConditionDemo { |
ReentrantLock
ReentrantLock 类是 Lock 接口的具体实现,与内置锁 synchronized 相同的是,它是一个可重入锁。
ReentrantLock 的特性
ReentrantLock 的特性如下:
ReentrantLock提供了与synchronized相同的互斥性、内存可见性和可重入性。ReentrantLock支持公平锁和非公平锁(默认)两种模式。ReentrantLock实现了Lock接口,支持了synchronized所不具备的灵活性。synchronized无法中断一个正在等待获取锁的线程synchronized无法在请求获取一个锁时无休止地等待
ReentrantLock 的用法
前文了解了 ReentrantLock 的特性,接下来,我们要讲述其具体用法。
ReentrantLock 的构造方法
ReentrantLock 有两个构造方法:
1 | public ReentrantLock() {} |
ReentrantLock()- 默认构造方法会初始化一个非公平锁(NonfairSync);ReentrantLock(boolean)-new ReentrantLock(true)会初始化一个公平锁(FairSync)。
lock 和 unlock 方法
lock()- 无条件获取锁。如果当前线程无法获取锁,则当前线程进入休眠状态不可用,直至当前线程获取到锁。如果该锁没有被另一个线程持有,则获取该锁并立即返回,将锁的持有计数设置为 1。unlock()- 用于释放锁。
🔔 注意:请务必牢记,获取锁操作
lock()必须在try catch块中进行,并且将释放锁操作unlock()放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
示例:ReentrantLock 的基本操作
1 | public class ReentrantLockDemo { |
输出结果:
1 | java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A] |
tryLock 方法
与无条件获取锁相比,tryLock 有更完善的容错机制。
tryLock()- 可轮询获取锁。如果成功,则返回 true;如果失败,则返回 false。也就是说,这个方法无论成败都会立即返回,获取不到锁(锁已被其他线程获取)时不会一直等待。tryLock(long, TimeUnit)- 可定时获取锁。和tryLock()类似,区别仅在于这个方法在获取不到锁时会等待一定的时间,在时间期限之内如果还获取不到锁,就返回 false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回 true。
示例:ReentrantLock 的 tryLock() 操作
修改上个示例中的 execute() 方法
1 | public void execute() { |
示例:ReentrantLock 的 tryLock(long, TimeUnit) 操作
修改上个示例中的 execute() 方法
1 | public void execute() { |
lockInterruptibly 方法
lockInterruptibly()- 可中断获取锁。可中断获取锁可以在获得锁的同时保持对中断的响应。可中断获取锁比其它获取锁的方式稍微复杂一些,需要两个try-catch块(如果在获取锁的操作中抛出了InterruptedException,那么可以使用标准的try-finally加锁模式)。- 举例来说:假设有两个线程同时通过
lock.lockInterruptibly()获取某个锁时,若线程 A 获取到了锁,则线程 B 只能等待。若此时对线程 B 调用threadB.interrupt()方法能够中断线程 B 的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
- 举例来说:假设有两个线程同时通过
注意:当一个线程获取了锁之后,是不会被
interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行状态中的线程,只能中断阻塞状态中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果未获取到锁,只有在等待的状态下,才可以响应中断。
示例:ReentrantLock 的 lockInterruptibly() 操作
修改上个示例中的 execute() 方法
1 | public void execute() { |
newCondition 方法
newCondition() - 返回一个绑定到 Lock 对象上的 Condition 实例。Condition 的特性和具体方法暂时不考虑
ReentrantLock 的原理
ReentrantLock 通过内存屏障和 happens-before 关系来保证可见性。
happens-before 是 Java 内存模型(JMM)中的核心概念,它定义了操作之间的内存可见性保证,是理解 Java 并发编程的基础。
什么是 Happens-Before?
Happens-Before 关系表示:如果操作 A happens-before 操作 B,那么:
- A 操作的结果对 B 操作可见
- A 操作按顺序排在 B 操作之前
🔔注意:这并不一定意味着在时间上 A 真的在 B 之前执行,而是指从内存可见性的角度来看,A 的效果对 B 是可见的。





