Unsafe.park vs Object.wait
Unsafe.park/unpark
和 Object.wait/notify
都可以用来实现线程的阻塞和唤醒,但两者有些本质的区别。
LockSupport
Unsafe.park
通常被用在 LockSupport
的 park
方法中,LockSupport
用于创建锁和其他同步类的基本线程阻塞原语。
AbstractQueuedSynchronizer(AQS)
框架中的方法大量使用该类来构建,该抽象类用于实现依赖先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件等),位于 java.util.concurrent
包下的大部分状态依赖类都构建于它之上,例如 ReentrantLock
、Semaphore
等。
此类与使用它的每个线程相关联一个许可(在 Semaphore
信号量类的意义上)。如果许可证可用,park
调用将立即返回,进程也将消费掉该许可;否则可能会阻塞。如果许可证不可用,则调用 unpark
可使许可证可用。(与信号量不同,许可不会累积。最多只有一个。)
方法 park
和 unpark
提供了阻塞和解除阻塞线程的有效方法,并且不会遇到导致过时方法 Thread.suspend
和 Thread.resume
因为以下目的变得不可用的问题:由于许可的存在,调用 park
的线程和另一个试图将其 unpark
的线程之间的竞争将保持活性。此外,如果调用者的线程被中断, park
将返回,并且支持超时版本。 park
方法还可以在其他任何时间由于虚假唤醒“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park
是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark
配对使用才更高效。
这些方法旨在用作创建更高级别同步实用程序的工具,并且它们本身对大多数并发控制应用程序没有用处。 park
方法通常的使用形式:
1 | while (!canProceed()) { |
Java docs 中的示例用法:先进先出非重入锁类的草图:
1 | class FIFOMutex { |
Object.wait/notify
Object 中的 wait
、notify
、 notifyAll
方法构成了内部条件队列的 API。一个对象的内部锁与它的内部条件队列是相关的:为了能够调用对象 X 中的任一个条件队列方法,你必须持有对象 X 的锁。
条件队列可以让一组线程 一一 称作等待集,以某种方式等待相关条件变成真,它也由此得名。不同于传统的队列,它们的元素是数据项,条件队列的元素是等待相关条件的线程。就像每个 Java 对象都能当作锁一样,每个对象也能当作条件队列。
这是因为“等待基于状态的条件”机制必须和“维护状态一致性”机制紧密地绑定在一起:除非你能检査状态,否则你不能等待条件;同时,除非你能改变状态,否则你不能从条件等待(队列)中释放其他的线程。
object.wait
会自动释放锁,并请求 OS(操作系统)挂起当前线程,让其他线程获得该锁进而修改对象的状态。当它被唤醒时,它会在返回前重新获得锁。直观上看,调用 wait
意味着“我要去休息了,但是发生了需要关注的事情后叫醒我”,调用通知(notify/notifyAll
)方法意味着“需要关注的事情发生了”。
1 | // 条件依赖方法的规范式 |
比较
他们都将挂起正在运行的线程并将其置于等待状态,有 3 种方法可以使线程处于 Thread.state.WAITING
状态:
- 没有超时的
Object.wait
- 没有超时的
Thread.join
- 没有超时的
LockSupport.park
但这两种方法的工作原理不同。 Object.wait()
方法适用于基于监视器的同步,在调用时会自动释放监视器锁,Unsafe.park 不会处理监视器锁。因此,这与 Java 中的“happens-before”关系很相配。为了将等待线程恢复为可运行状态,我们将在同一个监视器对象上使用 Object.notify()
方法。因此,当线程回到可运行状态时,它肯定会获得跨多个线程共享的变量的更新值。JVM 将确保线程状态与主内存同步,但这是额外的开销。
Unsafe.park()
方法将线程作为参数。要将 park
的线程变回可运行状态,我们需要在同一线程上调用 Unsafe.unpark()
方法。它在许可的基础上工作,许可与传入的线程关联,并且只有一个许可。当 Unsafe.unpark()
被调用时,如果线程已经被 park
,它将解除阻塞,或者将确保线程上的下一个 park
调用立即解除阻塞。所以它的性能应该更好,因为不需要与主存同步。 这就是为什么线程池(例如 ExecutorService
)在等待来自阻塞队列的任务时使用 park
方法的原因。
如您所见,这些用例是不同的。如果您有跨线程共享的状态并且您想确保一个线程在继续更新之前应该等待另一个线程,那么您应该继续使用 wait()
和 notify()
方法。作为应用程序开发人员,大多数情况下您不必使用 park()
方法,它的 API 级别太低。