问题历史
在Java 5之前,线程协调依赖于一些原始方法,如Thread.suspend(由于固有的死锁风险而不推荐使用)或Object.wait/notify,这要求严格的监视器所有权,并且在通知发生在等待之前时会遭遇丢失唤醒的问题。随着Java 5中引入的java.util.concurrent (JSR 166),LockSupport被设计为一种低级解阻原语,以便构建高性能的同步器,如AbstractQueuedSynchronizer,而不带有内置锁的负担。
问题
在并发编程中,当一个信号线程在目标线程实际阻塞之前调用解阻机制时,会发生经典的竞争条件。在传统的条件变量中,这个信号会丢失,导致目标线程无限期地处于睡眠状态。一个简单的解决方案可能使用计数信号量来累积许可证,但如果生产者的速度超过消费者,则会引入不必要的复杂性和潜在的资源泄漏。
解决方案
LockSupport使用与每个线程相关联的非累积单比特许可证。此许可证充当一次性、线程本地的通行证:
因为许可证不是累积的(它的最大值为1),它防止了由于过度唤醒而造成的内存泄漏,同时确保在阻塞之前发出的一个解阻呼叫会被记住,从而通过先行发生的关系消除丢失唤醒的问题。
import java.util.concurrent.locks.LockSupport; public class PermitExample { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("工作线程:初始工作..."); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("工作线程:尝试阻塞..."); LockSupport.park(); System.out.println("工作线程:成功解阻!"); }); worker.start(); // 在工作线程实际阻塞之前发出信号 Thread.sleep(50); System.out.println("主线程:在工作线程阻塞之前调用解阻"); LockSupport.unpark(worker); worker.join(); } }
问题描述
在设计高频交易系统的订单匹配引擎时,我们需要一种背压机制,允许消费者线程在入队列达到容量时暂停处理,而不持有会阻止生产者检查队列状态的锁。标准的ReentrantLock结合Condition在线程信号时会导致对队列锁的竞争,而Object.wait/notify在高频率竞争期间则面临着丢失唤醒的风险。
考虑的不同解决方案
1. Object.wait/notifyAll
这种方法使用队列的内置锁。优点:使用标准监视器实现简单。缺点:需要生产者获取监视器来调用notify,从而创建序列化瓶颈。更糟的是,如果一个生产者在消费者检查队列大小和调用wait之间的短暂窗口内调用notify,则信号会丢失,导致消费者永久死锁。
2. 带有多个条件的ReentrantLock
我们尝试为“满”和“空”状态分别使用条件。优点:比内置锁更灵活,允许选择性唤醒。缺点:信号化时仍需获取锁(signalAll),以及正确转移线程之间的条件队列引入了维护开销而未解决根本的锁开销。
3. 使用显式原子状态的LockSupport
所选解决方案使用一个AtomicBoolean来表示“继续的权限”,并使用LockSupport进行阻塞。当队列满时,消费者原子性地设置“需要停车”标志,然后进行阻塞。生产者在移除项目后检查该标志,并在设置时调用解阻。优点:信号化不需要任何锁,消除唤醒期间的竞争。单比特许可证模型确保即使生产者在消费者调用阻塞之前的纳秒内调用解阻,唤醒也不会丢失。
选定的解决方案及结果
我们选择了LockSupport方法。通过将信号机制与队列的结构性锁解耦,我们在重负荷下将生产者延迟减少了40%,并消除了在压力测试中观察到的丢失唤醒场景。显式状态管理(在解阻后双重检查条件)确保了尽管有park()的虚假唤醒合同,但仍然保持正确性。
调用LockSupport.park时是否释放线程持有的监视器的所有权?
不。这与Object.wait()之间有一个关键区别。当一个线程调用LockSupport.park时,它进入了一种等待状态,但保留了当前持有的所有监视器的所有权。如果另一个线程试图进入其中一个监视器(例如,同一对象上的同步块),它将被阻塞;如果被阻塞的线程是唯一可以释放的线程,可能导致死锁。候选人经常错误地假设park类似于wait并释放锁;这纯粹是一个线程本地的调度原语。
当在中断状态已设置的线程上调用LockSupport.park时,行为如何?
该方法立即返回而不阻塞,并且不清除中断状态。这与Object.wait()根本不同,后者会清除中断状态并抛出InterruptedException。使用LockSupport,线程必须显式检查并清除中断状态(通过Thread.interrupted()),如果希望遵循中断约定。此设计允许在非可中断上下文中使用park,或在中断与停车许可证分离的情况下处理中断。
LockSupport如何处理虚假唤醒,这对编码模式有何影响?**
LockSupport.park被记录为“无缘无故”返回(虚假唤醒),尽管在现代JVM中,这种情况在实践中是少见的。与基于许可证的唤醒(unpark)不同,虚假唤醒不会消耗许可证。因此,调用者必须始终在循环中重新检查造成停车的条件:
while (!canProceed()) { LockSupport.park(); }
候选人常常忽视,仅在park后检查一次条件是不够的;线程可能因虚假唤醒(或由于意外中断)而在没有解阻调用的情况下唤醒,需要重新评估状态条件。许可证确保有效的解阻不会丢失,但并不防止虚假返回。