
本文深入探讨了java并发编程中,多线程竞争`reentrantlock`时可能引发的线程饥饿问题。我们将分析通过线程休眠(固定时间或随机时间)来缓解饥饿的策略,并讨论其适用场景及局限性。在此基础上,文章将引入更高效、更公平的解决方案——利用`reentrantlock`的`condition`机制进行线程协作,从而有效避免资源饥饿,提升并发应用的健壮性和响应性。
在Java并发编程中,当多个线程需要访问共享资源时,通常会使用锁机制来保证数据一致性。ReentrantLock是一个常用的选择,它提供了比synchronized关键字更灵活的锁控制。然而,在某些场景下,尤其是一个线程长时间持有锁或以高频率尝试获取锁时,可能会导致其他线程长时间无法获取锁,从而产生线程饥饿(Thread Starvation)问题。
考虑一个典型场景:一个无限循环的线程持续执行任务并获取、释放一个ReentrantLock。同时,另有一个或多个调度任务线程(或通过其他方式触发的线程)也需要获取同一个锁来执行其任务。如果无限循环线程执行得过于频繁且不给其他线程留出足够的时间,其他线程可能会因为无法及时获取锁而长时间等待,甚至永远无法执行,这就是线程饥饿。
即使其他线程使用带超时的tryLock(timeout, TimeUnit)方法尝试获取锁,如果锁被持续占用,它们也可能反复超时失败。
为了缓解上述饥饿问题,一种直观的解决方案是在无限循环线程释放锁后,让其短暂休眠一段时间。这为其他等待获取锁的线程提供了一个机会窗口。
立即学习“Java免费学习笔记(深入)”;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockContentionExample {
private final Lock writeLock = new ReentrantLock(true); // 使用公平锁,有助于缓解饥饿
public void infiniteLoopTask() {
while (true) {
try {
// 尝试获取锁,设置超时时间以避免无限等待
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
doSomething(); // 执行核心业务逻辑
} finally {
writeLock.unlock(); // 确保锁在任何情况下都被释放
}
} else {
// 如果在超时时间内未能获取锁,可以做一些其他事情或直接继续循环
System.out.println(Thread.currentThread().getName() + " - 未能获取锁,重试...");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " - 任务中断。");
break;
}
// 释放锁后短暂休眠,给其他线程获取锁的机会
ThreadUtil.sleep(10); // 假设 ThreadUtil.sleep 是一个简单的休眠工具方法
}
}
public void scheduledTask() {
try {
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
doScheduledWork();
} finally {
writeLock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " - 调度任务未能获取锁。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " - 调度任务中断。");
}
}
private void doSomething() {
System.out.println(Thread.currentThread().getName() + " - 正在执行核心任务...");
// 模拟耗时操作
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void doScheduledWork() {
System.out.println(Thread.currentThread().getName() + " - 正在执行调度任务...");
// 模拟耗时操作
try {
Thread.sleep(5);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 假设的 ThreadUtil 工具类
static class ThreadUtil {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
LockContentionExample example = new LockContentionExample();
new Thread(example::infiniteLoopTask, "InfiniteLoopThread").start();
new Thread(example::scheduledTask, "ScheduledTaskThread-1").start();
new Thread(example::scheduledTask, "ScheduledTaskThread-2").start();
}
}在上述例子中,我们使用了固定时间(如10毫秒)的休眠。有人可能会提出使用随机休眠时间,例如 ThreadUtil.sleep(RandomUtil.nextInt(5, 100))。那么,这两种方式有何区别和优劣呢?
常量休眠 (ThreadUtil.sleep(10)):
随机休眠 (ThreadUtil.sleep(RandomUtil.nextInt(5, 100))):
总结:对于只有两个竞争线程的简单情况,常量休眠通常足够。但如果存在多个竞争线程,随机休眠可以更有效地防止某些线程因固定调度模式而陷入饥饿。
如果无限循环线程在释放锁后完全不休眠,其他线程是否仍会最终被执行?答案是:理论上会,但实际上可能会面临长时间的、不确定的延迟,这等同于饥饿。 JVM的线程调度器会尝试公平地分配CPU时间,但如果一个线程频繁且快速地获取、释放锁,并且在锁可用时总能第一时间再次获取,那么其他线程可能需要等待很长时间才能轮到它们获取锁,或者在它们的tryLock超时时间内锁从未被释放。
简单地通过休眠来“让出”CPU时间是一种启发式方法,但它不够精确,且可能导致性能浪费(如果休眠期间锁是空闲的,但没有线程被唤醒去获取)。更专业、更高效的解决方案是利用ReentrantLock提供的Condition对象进行线程间的精确协作。
Condition对象允许线程在特定条件下等待(await()),并在条件满足时被其他线程唤醒(signal()或signalAll())。这比盲目休眠或依赖调度器更加高效和可控。
工作原理:
示例代码(概念性):
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class CoordinatedLockAccess {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean resourceAvailable = false; // 共享资源状态
// 生产/提供资源线程
public void producerTask() {
lock.lock();
try {
// 模拟生产资源
System.out.println(Thread.currentThread().getName() + " - 正在生产资源...");
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
resourceAvailable = true; // 资源已准备好
condition.signalAll(); // 通知所有等待的消费者线程
System.out.println(Thread.currentThread().getName() + " - 资源生产完毕,通知消费者。");
} finally {
lock.unlock();
}
}
// 消费/使用资源线程
public void consumerTask() {
lock.lock();
try {
while (!resourceAvailable) { // 循环检查条件,防止虚假唤醒
System.out.println(Thread.currentThread().getName() + " - 资源未就绪,等待...");
try {
condition.await(); // 释放锁并等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 资源已就绪,进行消费
System.out.println(Thread.currentThread().getName() + " - 资源已就绪,开始消费。");
// 模拟消费资源
try { Thread.sleep(20); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
resourceAvailable = false; // 消费后重置状态
System.out.println(Thread.currentThread().getName() + " - 资源消费完毕。");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
CoordinatedLockAccess coordinator = new CoordinatedLockAccess();
// 启动消费者线程
new Thread(coordinator::consumerTask, "Consumer-1").start();
new Thread(coordinator::consumerTask, "Consumer-2").start();
// 稍后启动生产者线程
try { Thread.sleep(100); } catch (InterruptedException e) { }
new Thread(coordinator::producerTask, "Producer").start();
}
}在这个Condition的例子中,消费者线程不会忙循环或盲目休眠,而是当条件不满足时,优雅地进入等待状态,释放锁,直到生产者线程明确地通知它们。这大大提高了资源利用率和线程调度的公平性,有效避免了饥饿。
综上所述,解决ReentrantLock竞争下的线程饥饿问题,需要根据具体场景权衡性能与公平性。从简单的线程休眠到复杂的Condition机制,每种方法都有其适用范围。理解它们的原理和局限性,有助于我们构建健壮、高效的并发应用程序。
以上就是优化Java并发:ReentrantLock竞争与线程饥饿的避免策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号