一、锁的进化:从 synchronized 到 ReentrantLock
大家好,在多线程编程中,锁机制是保证线程安全的核心技术。Java 早期只提供了 synchronized 这一种内置锁,而在 JDK 1.5 后,Doug Lea 大师为我们带来了更加灵活强大的显式锁ReentrantLock
。
synchronized 虽然用起来简单,但在某些场景下会显得"能力不足":
- 无法响应中断请求
- 无法尝试获取锁
- 不支持公平性选择
- 通知机制基于单一等待队列,难以实现精准唤醒
这时,ReentrantLock
就成了我们的"救星"。让我们一起来深入了解这把锁!
二、ReentrantLock 的核心特性
ReentrantLock 是 Lock 接口的一个实现,它提供了比 synchronized 更丰富的功能:
2.1 可重入性
首先,什么是"可重入"?简单说就是:同一个线程可以多次获取同一把锁而不会死锁。
举个生活例子:小明进入自己房间后反锁了门,这时他想去卫生间,卫生间的门也是需要钥匙的,而这把钥匙就在小明口袋里。如果锁是"不可重入"的,那么小明就陷入了困境------他无法使用口袋里的钥匙,因为他已经在使用这把钥匙锁住了房门。
但在"可重入锁"的情况下,小明可以直接用同一把钥匙开卫生间的门,而不会有任何问题。
来看代码示例:
java
public class ReentrantDemo {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock(); // 第一次获取锁
try {
System.out.println("进入outer方法,当前线程:" + Thread.currentThread().getName());
inner(); // 调用inner方法
} finally {
lock.unlock(); // 释放锁
}
}
public void inner() {
lock.lock(); // 第二次获取锁(同一线程)
try {
System.out.println("进入inner方法,当前线程:" + Thread.currentThread().getName());
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantDemo demo = new ReentrantDemo();
demo.outer();
}
}
如果没有可重入特性,上面代码在调用 inner()方法时就会死锁!
为了更直观地理解可重入性的重要性,看一个模拟"不可重入锁"的例子:
java
public class NonReentrantLockDemo {
// 模拟一个不可重入锁
private static class NonReentrantLock {
private boolean isLocked = false;
private Thread lockedBy = null;
public synchronized void lock() throws InterruptedException {
// 不管是否是当前持有锁的线程,都要等待锁释放
while (isLocked) {
wait();
}
isLocked = true;
lockedBy = Thread.currentThread();
}
public synchronized void unlock() {
if (isLocked && Thread.currentThread() == lockedBy) {
isLocked = false;
lockedBy = null;
notify();
}
}
}
private static final NonReentrantLock nonReentrantLock = new NonReentrantLock();
public static void main(String[] args) throws InterruptedException {
nonReentrantLock.lock();
System.out.println("获取第一次锁");
try {
// 尝试再次获取锁
System.out.println("尝试获取第二次锁...");
nonReentrantLock.lock(); // 这里会永久阻塞!
System.out.println("获取第二次锁成功"); // 永远不会执行到这里
} finally {
nonReentrantLock.unlock();
}
}
}
运行这段代码会永久阻塞,因为第二次调用lock()
时,锁已被同一线程持有,但由于不支持重入,线程只能等待自己释放锁,形成死锁。这正是可重入性解决的问题。
2.2 公平锁与非公平锁
ReentrantLock 提供了两种获取锁的方式:公平锁和非公平锁。
- 公平锁:严格按照线程请求的顺序获取锁,类似于排队买票,先来先得
- 非公平锁:不保证等待时间最长的线程优先获取锁,允许"插队",默认模式
创建方式对比:
java
// 默认创建非公平锁
ReentrantLock unfairLock = new ReentrantLock();
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁的优点是显著降低了"饥饿"现象发生的概率,保证每个线程都有机会获取锁;缺点是整体吞吐量相对较低。非公平锁则允许更充分地利用 CPU 资源,但可能导致某些线程长时间等待。
需要注意的是,即使使用公平锁,也无法完全杜绝饥饿现象,因为线程可能因为其他原因(如中断或取消)退出等待队列。
场景选择建议:
- 在高并发且线程生命周期较短的场景中,非公平锁通常表现更好,因为新线程可以立即尝试获取锁,减少上下文切换
- 在线程任务执行时间差异大、并且某些线程优先级较低的系统中,公平锁可以减少低优先级线程的饥饿概率
- 对于需要严格保证请求顺序的系统(如排队系统),公平锁是更合适的选择
2.3 多种获取锁的方式
ReentrantLock 提供了多种获取锁的方式,大大增强了灵活性:
- lock():最基本的获取锁方法,如果锁被占用,会一直等待
- tryLock():尝试获取锁,立即返回结果(成功/失败),不会阻塞
- tryLock(long timeout, TimeUnit unit):在指定时间内尝试获取锁
- lockInterruptibly():可中断的获取锁,允许在等待时响应中断信号
我们可以用一个餐厅排队的例子来理解:
lock()
:不管多久我都要等到有位置tryLock()
:看一眼有没有空位,有就坐,没有就走tryLock(time)
:最多等 30 分钟,如果还没位置就去别家lockInterruptibly()
:等位过程中如果接到重要电话可以中途离开
2.4 精准通知机制:Condition
ReentrantLock 结合 Condition 接口,提供了比 synchronized + wait/notify 更加强大的线程通信能力:
与 synchronized 相比的优势:
- 一个锁可以创建多个 Condition 对象,实现"选择性通知"
- 更精准的线程控制,避免了 Object.notify()的盲目唤醒
- 提供带超时的等待和可中断的等待
信号类型对比:
- signal():只唤醒单个等待该条件的线程,适用于只需要唤醒一个消费者/生产者的场景
- signalAll():唤醒所有等待该条件的线程,适用于需要通知所有相关线程的状态变更场景
重要提示 :Condition
的await()
和signal()
方法必须在持有锁的情况下调用,否则会抛出IllegalMonitorStateException
。这一点与synchronized
中的wait()
/notify()
要求一致。
Condition 还提供了带超时的等待方法:
- await(long time, TimeUnit unit):在指定时间内等待,超时或被通知则返回
这进一步增强了线程等待的灵活性,避免了无限期阻塞的风险。
2.5 锁状态查询能力
ReentrantLock 提供了一系列查询锁状态的方法,这在调试和监控中非常有用:
- isLocked():查询锁是否被任何线程持有
- isHeldByCurrentThread():查询当前线程是否持有锁
- getHoldCount():查询当前线程持有锁的次数(重入次数)
- getQueueLength():获取等待获取此锁的线程数
- hasQueuedThread(Thread t):查询指定线程是否在等待队列中
这些方法让我们能够更精确地了解锁的使用状态,在复杂并发场景中进行故障排查。
三、ReentrantLock 实战案例
3.1 案例 1:实现可中断的获取锁
当多个线程竞争锁时,如果使用lockInterruptibly()
方法,我们可以实现提前结束等待状态,避免死锁:
java
public class InterruptibleLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
try {
lock.lock();
System.out.println("线程1获取到锁,将无限期持有...");
// 模拟长时间持有锁
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
System.out.println("线程1被中断");
// 此处不恢复中断状态,因为线程需要继续持有锁而不被中断
} finally {
lock.unlock();
System.out.println("线程1释放锁");
}
});
thread1.start();
Thread.sleep(500); // 确保线程1先获取到锁
Thread thread2 = new Thread(() -> {
System.out.println("线程2尝试获取锁...");
try {
// 可中断的获取锁
lock.lockInterruptibly();
System.out.println("线程2获取到锁");
} catch (InterruptedException e) {
System.out.println("线程2等待锁的过程被中断了");
// 恢复中断状态
Thread.currentThread().interrupt();
}
});
thread2.start();
Thread.sleep(1000); // 给线程2一些时间尝试获取锁
// 中断线程2的等待
System.out.println("主线程决定中断线程2的等待");
thread2.interrupt();
// 等待线程2处理完中断
thread2.join();
System.out.println("程序结束");
}
}
输出结果:
erlang
线程1获取到锁,将无限期持有...
线程2尝试获取锁...
主线程决定中断线程2的等待
线程2等待锁的过程被中断了
程序结束
这个案例说明:使用lockInterruptibly()
可以避免线程无限期地等待锁,增强了程序的可控性。相比之下,如果使用lock()
方法,线程 2 将无法响应中断,只能一直等待。
3.2 案例 2:使用 tryLock 实现超时等待
在一些对时间敏感的系统中,无限期等待锁可能导致严重问题。使用tryLock()
方法可以设置等待超时时间:
java
public class TryLockDemo {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
try {
lock.lock();
System.out.println("线程1获取到锁");
// 模拟持有锁的工作
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
// 恢复中断状态
Thread.currentThread().interrupt();
} finally {
lock.unlock();
System.out.println("线程1释放锁");
}
});
thread1.start();
// 确保线程1先获取到锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
// 恢复中断状态
Thread.currentThread().interrupt();
}
Thread thread2 = new Thread(() -> {
boolean acquired = false;
try {
System.out.println("线程2尝试获取锁,最多等待2秒");
// 尝试在2秒内获取锁
acquired = lock.tryLock(2, TimeUnit.SECONDS);
if (acquired) {
System.out.println("线程2成功获取到锁");
// 模拟工作
Thread.sleep(1000);
} else {
System.out.println("线程2获取锁失败,执行备选方案");
// 执行其他操作...
}
} catch (InterruptedException e) {
e.printStackTrace();
// 重要:恢复中断状态,以便调用者能够检测到中断
Thread.currentThread().interrupt();
} finally {
if (acquired) {
lock.unlock();
System.out.println("线程2释放锁");
}
}
});
thread2.start();
}
}
注意上面代码中,当捕获InterruptedException
时,我们调用了Thread.currentThread().interrupt()
来恢复线程的中断状态。这是因为异常被捕获后,线程的中断状态会被清除,而恢复中断状态可以让上层调用者知道线程曾经被中断过。
输出结果:
线程1获取到锁
线程2尝试获取锁,最多等待2秒
线程2获取锁失败,执行备选方案
线程1释放锁
这个案例演示了如何避免线程长时间等待,提高系统的响应性。tryLock
方法在分布式系统或微服务架构中特别有用,可以防止级联阻塞。
3.3 案例 3:使用 Condition 实现精准线程通信
使用 Condition 可以实现更精细的线程控制,下面是一个使用多个 Condition 实现的有界缓冲区示例,并演示了 Condition 的超时等待特性:
java
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 缓冲区不满条件
private final Condition notEmpty = lock.newCondition(); // 缓冲区不空条件
private final Object[] items;
private int putIndex, takeIndex, count;
public BoundedBuffer(int capacity) {
items = new Object[capacity];
}
// 存入数据
public void put(Object x) throws InterruptedException {
lock.lock();
try {
// 使用while循环检查条件,防止虚假唤醒
while (count == items.length) {
System.out.println(Thread.currentThread().getName() + " 发现缓冲区已满,等待...");
notFull.await(); // 必须在持有锁的状态下调用
}
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
++count;
System.out.println(Thread.currentThread().getName() + " 放入数据: " + x +
",当前缓冲区数据量: " + count);
// 通知消费者可以取数据了
notEmpty.signal(); // 精确通知等待缓冲区不空的线程
} finally {
lock.unlock();
}
}
// 取出数据(带超时)
public Object takeWithTimeout(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
// 计算截止时间
long nanos = unit.toNanos(timeout);
// 使用while循环检查条件
while (count == 0) {
System.out.println(Thread.currentThread().getName() + " 发现缓冲区为空,尝试等待" +
timeout + unit.toString().toLowerCase() + "...");
if (nanos <= 0) {
// 超时退出
System.out.println(Thread.currentThread().getName() + " 等待超时,返回null");
return null;
}
// 带超时的等待,返回剩余等待时间
nanos = notEmpty.awaitNanos(nanos);
}
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
--count;
System.out.println(Thread.currentThread().getName() + " 取出数据: " + x +
",当前缓冲区数据量: " + count);
// 通知生产者可以放数据了
notFull.signal(); // 精确通知等待缓冲区不满的线程
return x;
} finally {
lock.unlock();
}
}
// 唤醒所有等待的生产者(示例signalAll()用法)
public void signalAllProducers() {
lock.lock();
try {
System.out.println("唤醒所有等待的生产者线程");
notFull.signalAll(); // 唤醒所有等待"不满"条件的线程
} finally {
lock.unlock();
}
}
// 原始的取出方法
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println(Thread.currentThread().getName() + " 发现缓冲区为空,等待...");
notEmpty.await();
}
Object x = items[takeIndex];
if (++takeIndex == items.length) takeIndex = 0;
--count;
System.out.println(Thread.currentThread().getName() + " 取出数据: " + x +
",当前缓冲区数据量: " + count);
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
BoundedBuffer buffer = new BoundedBuffer(3);
// 生产者线程(速度较慢)
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
Thread.sleep(500); // 生产慢一点,让消费者体验超时
buffer.put(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}, "生产者");
// 消费者线程(带超时)
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
// 超时等待2秒
Object item = buffer.takeWithTimeout(2, TimeUnit.SECONDS);
if (item == null) {
System.out.println("消费者因超时放弃等待,循环次数: " + i);
}
Thread.sleep(100); // 消费速度快一些
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
}, "消费者");
consumer.start(); // 先启动消费者,这样必然会遇到空缓冲区
try {
Thread.sleep(1000); // 让消费者先等一会儿
} catch (InterruptedException e) {
e.printStackTrace();
}
producer.start(); // 后启动生产者
}
}
上面代码中有几个关键点需要特别注意:
-
使用 while 而非 if 检查条件 :这是防止虚假唤醒(Spurious Wakeup)。线程可能在没有被显式唤醒的情况下从
await()
返回,使用 while 循环确保条件确实满足。 -
await()
和signal()
必须在持有锁的情况下调用 :这与synchronized
中的wait()
/notify()
一样,是线程安全的基本要求。 -
精确通知 :
notFull.signal()
只会唤醒等待"不满"条件的生产者线程,notEmpty.signal()
只会唤醒等待"不空"条件的消费者线程。这比synchronized
中的notify()
更有针对性。 -
超时等待 :
takeWithTimeout
方法展示了如何使用Condition.awaitNanos()
实现带超时的等待,避免了消费者无限期等待的问题。 -
信号类型选择 :示例中还展示了
signalAll()
方法的用法,当需要唤醒多个等待线程时(如清空缓冲区操作),应使用signalAll()
而非signal()
。
四、ReentrantLock 底层原理探秘
ReentrantLock 的强大功能离不开其底层实现机制------AQS(AbstractQueuedSynchronizer)。
AQS 内部维护了一个 volatile 变量 state 和一个 FIFO 的等待队列。对于 ReentrantLock:
- state = 0 表示锁空闲
- state > 0 表示锁被占用,值记录了重入次数
- 当一个线程获取锁失败时,它会被包装成一个 Node 加入 FIFO 队列
- 队列中的节点有不同状态(如 CANCELLED、SIGNAL 等),AQS 通过这些状态管理线程的阻塞与唤醒,避免无效竞争
- 释放锁时会唤醒队列中的后继节点
在非公平锁实现中,新到来的线程可以直接尝试 CAS 获取锁,而不必排队;在公平锁实现中,线程必须先检查队列中是否有前驱节点,只有没有前驱时才能尝试获取锁。
这种机制使得 ReentrantLock 能够高效地管理锁竞争,并支持公平或非公平获取锁的策略。
五、ReentrantLock 使用注意事项
5.1 必须手动释放锁
与 synchronized 不同,ReentrantLock 要求手动释放锁,通常的模式是:
java
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 确保锁被释放
}
为什么要放在 finally 块中? 防止临界区代码抛出异常而导致锁无法释放,进而引发死锁。这是使用 ReentrantLock 最容易出错的地方,必须养成良好习惯。
5.2 公平锁与非公平锁的选择
- 非公平锁(默认):吞吐量更高,但可能造成线程饥饿
- 公平锁:等待更公平,但整体性能较低
根据 Oracle JDK 的官方基准测试,在高竞争环境下,公平锁的吞吐量比非公平锁低约 10%-20%。这是因为公平锁需要维护一个严格的 FIFO 队列,额外的检查和同步开销导致性能下降。
一般情况下使用默认的非公平锁即可,除非系统特别需要保证每个线程的公平性。
5.3 性能考量
ReentrantLock 相比 synchronized 在不同场景下的性能表现:
- 低竞争场景:JDK 1.6 后对 synchronized 进行了大量优化(偏向锁、轻量级锁),在低竞争情况下,synchronized 性能接近甚至优于 ReentrantLock
- 高竞争场景:ReentrantLock 的灵活性(如超时获取、可中断)和精确的线程控制能带来更好的整体性能
选择时应考虑实际应用场景和锁竞争的激烈程度。
六、ReentrantLock vs synchronized
来看看它们的主要区别:
特性 | ReentrantLock | synchronized |
---|---|---|
锁获取方式 | 显式(lock()) | 隐式(进入同步块) |
锁释放方式 | 显式(unlock()) | 隐式(离开同步块) |
锁类型 | 接口实现,可以继承 | 关键字,内置语言特性 |
可中断获取 | 支持(lockInterruptibly()) | 不支持 |
超时获取 | 支持(tryLock(time)) | 不支持 |
公平性 | 可选择(默认非公平) | 非公平 |
多条件变量 | 支持(Condition) | 不支持(只有一个等待集合) |
性能(低竞争) | 较好 | JDK 1.6 优化后较好 |
性能(高竞争) | 较好 | JDK 1.6 优化后接近 |
锁状态检查 | 支持(isLocked()等) | 不支持 |
编码复杂度 | 较高(需手动解锁) | 较低(自动解锁) |
七、ReentrantLock 进阶案例:可重入读写锁
在某些场景下,我们需要区分读操作和写操作的锁定粒度。ReentrantReadWriteLock 提供了这种能力:
java
public class ReadWriteLockDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private final Map<String, String> data = new HashMap<>();
// 写操作:独占锁
public void put(String key, String value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
// 模拟写入耗时
Thread.sleep(1000);
data.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入完成: " + key + "=" + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
// 读操作:共享锁
public String get(String key) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
// 模拟读取耗时
Thread.sleep(200); // 读操作比写操作快,更能体现读共享优势
String value = data.get(key);
System.out.println(Thread.currentThread().getName() + " 读取完成: " + key + "=" + value);
return value;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
readLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 预先放入一些数据
demo.put("key1", "value1");
// 创建10个读线程,更好地展示读并发效果
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
demo.get("key1");
}, "读线程" + index).start();
}
// 创建2个写线程
for (int i = 0; i < 2; i++) {
final int index = i;
new Thread(() -> {
demo.put("key" + (index + 2), "value" + (index + 2));
}, "写线程" + index).start();
}
}
}
关键点:
- 写锁是独占的:一次只能有一个线程获取写锁
- 读锁是共享的:多个线程可以同时获取读锁
- 写锁和读锁互斥:有写锁时不能获取读锁,有读锁时不能获取写锁
- 适合"读多写少"的场景
7.1 锁降级:从写锁降级为读锁
一个重要但常被忽略的技巧是锁降级,即持有写锁的线程可以获取读锁,然后释放写锁,这样就从写锁降级为读锁了:
java
public class LockDegradingDemo {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, Object> cacheMap = new HashMap<>();
// 使用锁降级更新缓存
public Object processCachedData(String key) {
Object value = null;
// 首先获取读锁查询缓存
readLock.lock();
try {
value = cacheMap.get(key);
if (value == null) {
// 缓存未命中,释放读锁,获取写锁
readLock.unlock();
writeLock.lock();
try {
// 再次检查,因为可能其他线程已经更新了缓存
value = cacheMap.get(key);
if (value == null) {
// 模拟从数据库加载数据
value = loadFromDatabase(key);
cacheMap.put(key, value);
System.out.println("缓存更新完毕: " + key);
}
// 锁降级:持有写锁的同时获取读锁
readLock.lock();
} finally {
// 释放写锁,保留读锁
writeLock.unlock();
}
// 此时线程仍持有读锁
}
// 使用读锁保护的数据
return value;
} finally {
readLock.unlock();
}
}
private Object loadFromDatabase(String key) {
System.out.println("从数据库加载: " + key);
// 模拟耗时操作
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "DB_" + key + "_VALUE";
}
public static void main(String[] args) {
LockDegradingDemo demo = new LockDegradingDemo();
// 多线程并发访问
for (int i = 0; i < 5; i++) {
final String key = "key" + (i % 2); // 只使用两个不同的key,增加并发更新的可能
new Thread(() -> {
Object value = demo.processCachedData(key);
System.out.println(Thread.currentThread().getName() + " 获取到: " + key + "=" + value);
}, "Thread-" + i).start();
}
}
}
锁降级的好处是保证数据的可见性。在更新完数据后,如果我们先释放写锁再获取读锁,那么在这个短暂的时间窗口内,可能有其他线程修改了数据。通过锁降级,我们确保读取的是自己最新写入的数据。
八、总结
通过本文的讲解,我们全面了解了 ReentrantLock 的高级特性与应用。下表总结了 ReentrantLock 的关键特性和应用场景:
特性 | 方法 | 适用场景 | 注意事项 |
---|---|---|---|
基本锁获取 | lock() | 一般同步场景 | 必须在 finally 中解锁 |
可重入性 | 内置特性 | 递归调用、嵌套锁 | 调用 unlock 次数必须等于 lock 次数 |
尝试获取锁 | tryLock() | 避免死锁、提高响应性 | 结果为 false 时需有备选方案 |
可中断锁获取 | lockInterruptibly() | 需要中断能力的场景 | 抛出 InterruptedException 后恢复中断状态 |
超时锁获取 | tryLock(time, unit) | 限时等待场景 | 超时返回 false |
公平性控制 | 构造函数参数 | 需要减少饥饿的场景 | 公平锁性能约低 10%-20% |
条件变量 | newCondition() | 复杂线程协作 | await 前必须持有锁,使用 while 循环检查条件 |
超时等待 | await(time, unit) | 需限时等待的场景 | 返回值表示是否超时 |
锁状态查询 | isLocked()等 | 调试和监控 | 结果可能立即过时 |
读写锁分离 | ReentrantReadWriteLock | 读多写少的场景 | 写锁可降级为读锁,反之不可 |
最后,记住一条黄金法则:锁的范围要尽可能小,持有时间要尽可能短。这样能最大限度地减少线程间的竞争,提高程序的并发性能。
在实际项目中,根据业务需求的不同,灵活选择合适的锁机制,才能构建高效、稳定的多线程应用!
在下一篇文章中,我们将探讨"线程间通信的三种经典方式",敬请期待!
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~