
本文探讨了多线程环境下,尤其是一个长时间运行的线程持有锁时,如何避免其他线程出现饥饿问题。通过分析线程休眠(固定时间与随机时间)的优缺点,以及更高级的`wait/notifyAll`机制(或`Condition`对象),文章旨在提供一套完整的解决方案,帮助开发者优化线程调度,确保共享资源的公平访问。
在并发编程中,多个线程竞争访问共享资源是常见场景。当一个线程长时间持有锁,而其他线程无法及时获取锁来执行其任务时,就会发生线程饥饿。这在存在无限循环或长时间执行任务的线程时尤为突出,可能导致系统响应迟缓或功能异常。本文将深入探讨如何通过线程休眠和更高级的同步机制来有效避免此类问题。
为了确保持有锁的线程在完成其关键任务后能适时释放CPU资源,给其他等待线程提供获取锁的机会,一种直观的方法是在释放锁后让该线程短暂休眠。
考虑一个线程在一个无限循环中执行代码,并且每次迭代都需要获取一个ReentrantLock。为了避免其他需要该锁的调度任务或手动触发任务被长时间阻塞,可以在释放锁后让该线程休眠一小段时间。
立即学习“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(); // 确保锁被释放
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Infinite loop thread interrupted.");
break;
} catch (Exception e) {
System.err.println("Error in infinite loop task: " + e.getMessage());
}
// 关键步骤:休眠一小段时间,让其他线程有机会获取锁
ThreadUtil.sleep(10); // 假设 ThreadUtil.sleep 是一个简单的休眠工具方法
}
}
private void doSomething() throws InterruptedException {
// 模拟实际业务逻辑
System.out.println(Thread.currentThread().getName() + " acquired lock and doing something.");
Thread.sleep(50); // 模拟业务执行时间
}
// 假设其他线程(如调度任务)也会调用此方法
public void scheduledTask() {
try {
if (writeLock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " acquired lock for scheduled task.");
// 执行调度任务
} finally {
writeLock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " failed to acquire lock for scheduled task.");
}
} 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();
}
}
// 简单的 ThreadUtil 类,用于模拟休眠
class ThreadUtil {
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}在这种两线程(或少量线程)竞争的简单场景下,固定时间的休眠(如ThreadUtil.sleep(10))是有效的。它能确保无限循环线程在每次迭代后释放CPU,允许其他线程尝试获取锁。其他线程通过tryLock(timeout, TimeUnit)尝试获取锁,如果锁被占用,它们会等待一小段时间,如果仍未获取到,则会放弃并稍后重试。
当竞争锁的线程数量增加(例如,三个或更多线程:一个无限循环线程A,两个竞争线程B和C),且它们的调度是可预测时,固定休眠时间可能会导致新的饥饿问题。例如,如果线程A释放锁后,线程B总是比线程C先尝试获取锁,并且每次都能成功,那么线程C将永远无法获取到锁。
为了打破这种可预测性并确保所有竞争线程都有机会获取锁,引入随机休眠时间变得有益。例如,ThreadUtil.sleep(RandomUtil.nextInt(5, 100))。通过让线程在不同的时间长度内休眠,可以增加线程A释放锁后,线程B和线程C竞争的随机性,从而降低特定线程持续饥饿的风险。
注意事项:
虽然线程休眠可以作为一种简单的缓解策略,但它并非最优雅或最高效的解决方案。Java提供了更强大的同步机制——Object.wait()和Object.notifyAll(),或者与ReentrantLock配合使用的Condition对象,它们能提供更细粒度的控制和更高的效率。
wait()和notifyAll()是基于对象监视器(monitor)的机制。当一个线程调用wait()时,它会释放当前持有的对象锁,并进入等待状态,直到被notify()或notifyAll()唤醒。
对于ReentrantLock,其等价的机制是Condition对象。Condition对象允许线程在特定条件不满足时等待,并在条件满足时被唤醒。它提供了await()(类似于wait())、signal()(类似于notify())和signalAll()(类似于notifyAll())方法。
这种机制的优势在于:
使用Condition的示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionSynchronizationExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean resourceReady = false; // 共享资源状态,表示资源是否可用
// 生产者线程(无限循环任务)
public void producerTask() {
while (true) {
lock.lock(); // 获取锁
try {
// 模拟生产资源
System.out.println(Thread.currentThread().getName() + " is producing resource...");
ThreadUtil.sleep(100); // 模拟生产时间
resourceReady = true; // 资源已准备好
condition.signalAll(); // 通知所有等待的消费者线程
} finally {
lock.unlock(); // 释放锁
}
ThreadUtil.sleep(50); // 生产者线程稍作休息,避免过于频繁地生产
}
}
// 消费者线程(调度任务或手动触发任务)
public void consumerTask(String taskName) {
lock.lock(); // 获取锁
try {
while (!resourceReady) { // 如果资源未准备好,则等待
System.out.println(taskName + " is waiting for resource...");
condition.await(); // 释放锁并等待,直到被signalAll唤醒
}
// 资源已准备好,消费资源
System.out.println(taskName + " consumed resource!");
resourceReady = false; // 重置状态,等待下一次生产
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(taskName + " interrupted while waiting.");
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ConditionSynchronizationExample example = new ConditionSynchronizationExample();
new Thread(example::producerTask, "ProducerThread").start();
new Thread(() -> example.consumerTask("ConsumerTask-1"), "ConsumerThread-1").start();
new Thread(() -> example.consumerTask("ConsumerTask-2"), "ConsumerThread-2").start();
}
}在这个例子中,producerTask在完成其工作后,通过condition.signalAll()通知所有在condition.await()上等待的consumerTask。消费者线程在获取到锁并发现resourceReady为false时,会调用await(),从而释放锁并进入等待状态,而不是忙循环或休眠。当资源准备好时,它们会被唤醒并再次尝试获取锁。
选择哪种策略取决于具体的应用场景、性能要求以及并发的复杂性。通常,推荐使用Condition对象与ReentrantLock结合的方式,因为它提供了最强大、最灵活且最符合Java并发编程范式的解决方案。
以上就是Java线程饥饿与锁竞争:策略与最佳实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号