并发编程原理与实战(二十四)Java并发基石LockSupport park/unpark机制全解析
前面的文章我们已经学习了synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock等多种锁,这些锁都是多线程并发协同的重要工具,本文来学习另一个提供了一种低开销、高灵活的多线程控制方式的工具LockSupport,Java并发工具链的底层基石之一。
什么是LockSupport
/*** Basic thread blocking primitives for creating locks and other* synchronization classes.** <p>This class associates, with each thread that uses it, a permit* (in the sense of the {@link java.util.concurrent.Semaphore* Semaphore} class). A call to {@code park} will return immediately* if the permit is available, consuming it in the process; otherwise* it <em>may</em> block. A call to {@code unpark} makes the permit* available, if it was not already available. (Unlike with Semaphores* though, permits do not accumulate. There is at most one.)* Reliable usage requires the use of volatile (or atomic) variables* to control when to park or unpark. Orderings of calls to these* methods are maintained with respect to volatile variable accesses,* but not necessarily non-volatile variable accesses.*/
LockSupport用于创建锁及其他同步类的基础线程阻塞原语。
本类为每个调用线程关联一个许可(概念类似于 java.util.concurrent.Semaphore信号量)。调用park方法时,若许可可用则立即返回并消耗该许可;否则线程可能阻塞直到许可可用(park的意思是把…搁置,就是让线程搁置的意思)。调用unpark则使许可变为可用状态(若尚未可用),即释放许可。与信号量不同,许可不可累积,每个线程最多持有一个。可靠使用需配合volatile或原子变量控制park/unpark调用时序。这些方法的调用顺序与volatile变量访问具有内存可见性保证,但与非volatile变量访问无必然关联。
LockSupport实现的线程阻塞/唤醒机制
/* <p>Methods {@code park} and {@code unpark} provide efficient* means of blocking and unblocking threads that do not encounter the* problems that cause the deprecated methods {@code Thread.suspend}* and {@code Thread.resume} to be unusable for such purposes: Races* between one thread invoking {@code park} and another thread trying* to {@code unpark} it will preserve liveness, due to the* permit. Additionally, {@code park} will return if the caller's* thread was interrupted, and timeout versions are supported. The* {@code park} method may also return at any other time, for "no* reason", so in general must be invoked within a loop that rechecks* conditions upon return. In this sense {@code park} serves as an* optimization of a "busy wait" that does not waste as much time* spinning, but must be paired with an {@code unpark} to be* effective.*/
park和unpark方法提供了高效的线程阻塞/唤醒机制,避免了弃用方法 Thread.suspend和Thread.resume的固有缺陷:由于许可机制的存在,当线程A调用park与线程B尝试unpark A时,不会出现死锁问题。此外,若调用线程被中断,park会立即返回,同时支持超时版本。需注意park可能因"未知原因"虚假唤醒,因此通常应在循环中调用并重新检查条件。在此意义上,park是对"忙等待"的优化,减少CPU空转,但必须配合unpark使用才能生效。
/* <p>The three forms of {@code park} each also support a* {@code blocker} object parameter. This object is recorded while* the thread is blocked to permit monitoring and diagnostic tools to* identify the reasons that threads are blocked. (Such tools may* access blockers using method {@link #getBlocker(Thread)}.)* The use of these forms rather than the original forms without this* parameter is strongly encouraged. The normal argument to supply as* a {@code blocker} within a lock implementation is {@code this}.*/
park方法的三种重载形式均支持blocker对象参数。该对象会在线程阻塞时被记录,便于监控诊断工具识别线程阻塞原因(工具可通过getBlocker(Thread)获取 blocker 对象)。强烈建议使用带blocker参数的版本。在锁实现中,通常传入this作为blocker参数。
/* <p>These methods are designed to be used as tools for creating* higher-level synchronization utilities, and are not in themselves* useful for most concurrency control applications. The {@code park}* method is designed for use only in constructions of the form:*/
这些方法旨在作为构建高级同步工具的基础设施,其本身并不适用于大多数常规并发控制场景。park方法仅推荐以下列形式使用:
//循环检查条件
while (!canProceed()) { // 确保unpark请求对其他线程可见 ... LockSupport.park(this);
}
从上面的建议的写法来看,为了防止伪唤醒的问题,必须使用while循环不断检查条件。
/* where no actions by the thread publishing a request to unpark,* prior to the call to {@code park}, entail locking or blocking.* Because only one permit is associated with each thread, any* intermediary uses of {@code park}, including implicitly via class* loading, could lead to an unresponsive thread (a "lost unpark").*/
调用park前,线程发布unpark请求的操作不得包含加锁或阻塞行为。由于每个线程仅关联一个许可,任何中间步骤(包括类加载隐式触发的park)均可能导致线程无响应(即“许可丢失”问题)。
基于LockSupport实现的锁
示例:先进先出非可重入锁
class FIFOMutex {private final AtomicBoolean locked = new AtomicBoolean(false);//等待队列 private final Queue<Thread> waiters = new ConcurrentLinkedQueue<>();//上锁方法public void lock() {boolean wasInterrupted = false;// 当前线程加入等待队列waiters.add(Thread.currentThread());// 循环检查:当前线程非队首线程或CAS抢锁失败时阻塞while (waiters.peek() != Thread.currentThread() ||!locked.compareAndSet(false, true)) {LockSupport.park(this);// 等待过程忽略中断请求,记录中断状态if (Thread.interrupted())wasInterrupted = true;}//抢锁成功移出等待队列waiters.remove();// 恢复中断状态if (wasInterrupted)Thread.currentThread().interrupt();}//释放锁方法 public void unlock() {//释放锁状态 locked.set(false);//唤醒队首等待线程 LockSupport.unpark(waiters.peek());}static {// 预加载LockSupport类Class<?> ensureLoaded = LockSupport.class;}}
这段代码使用LockSupport+AtomicBoolean+ConcurrentLinkedQueue实现了一个轻量级、公平的互斥锁。
- 公平性:通过队列保证线程按申请顺序获取锁(对比synchronized的非公平性)。
- 低竞争:基于LockSupport,避免synchronized的锁膨胀问题(即随着竞争加剧从低级别锁逐步升级为高级别锁的过程)。
- 轻量级:仅依赖AtomicBoolean和并发队列ConcurrentLinkedQueue实现。
(1)lock()方法关键逻辑:
队列控制:只有队首线程有资格尝试获取锁(waiters.peek() == currentThread)。
CAS抢锁:compareAndSet(false, true)保证只有一个线程能修改锁状态,能将锁状态修改为true即抢锁成功。
中断处理:阻塞期间忽略中断,但最终恢复中断标记(避免信号丢失)。
(2)unlock()方法关键逻辑:
唤醒机制:直接通知队列中的下一个等待线程,避免无效竞争(精准唤醒某个线程,对比notifyAll把全部等待状的线程都唤醒造成"惊群效应")。
(3)静态初始化块设计
预加载LockSupport类,目的是防止类加载延迟导致unpark丢失。unpark丢失是指在多线程编程中,唤醒操作(unpark)未能生效,导致后续的等待操作(park)出现不必要的阻塞现象。首次调用LockSupport时,JVM需加载原生类库。若此时线程A调用unpark(threadB)释放一个许可信号,而LockSupport类尚未加载完成,导致信号未被记录,后续threadB.park()获取不到信号将永久阻塞。
LockSupport API详解
LockSupport 提供的api不多,主要分为两类,一类是阻塞类方法,一类是唤醒类方法。
阻塞类
1、park()
/*** Disables the current thread for thread scheduling purposes unless the* permit is available.** <p>If the permit is available then it is consumed and the call* returns immediately; otherwise the current thread becomes disabled* for thread scheduling purposes and lies dormant until one of three* things happens:** <ul>** <li>Some other thread invokes {@link #unpark unpark} with the* current thread as the target; or** <li>Some other thread {@linkplain Thread#interrupt interrupts}* the current thread; or** <li>The call spuriously (that is, for no reason) returns.* </ul>** <p>This method does <em>not</em> report which of these caused the* method to return. Callers should re-check the conditions which caused* the thread to park in the first place. Callers may also determine,* for example, the interrupt status of the thread upon return.*/
public static void park() {if (Thread.currentThread().isVirtual()) {VirtualThreads.park();} else {U.park(false, 0L);}
}
这是一个静态的方法,调用该方法,若许可可用则立即消耗该许可并返回;否则当前线程将被禁用线程调度,进入休眠状态直至以下三种情况之一发生:
(1)其他线程调用unpark并以当前线程为目标;
(2)其他线程interrupt 中断当前线程;
(3)调用虚假返回(即无明确原因返回)。
2、park(Object blocker)
park(Object blocker),带阻塞对象标识的park(),便于诊断。blocker 是导致当前线程停驻的同步对象,用于诊断线程阻塞原因的同步对象标识,主要作用是为监控工具提供上下文信息。Java官方推荐使用带blocker的park方法替代无参版本,尤其在锁实现中通常传入this作为阻塞标识。如:
void lock() {Thread current = Thread.currentThread();while (!owner.compareAndSet(null, current)) {// 阻塞时记录当前锁实例为blockerLockSupport.park(this); // this作为blocker}
}
当线程因竞争锁失败被park时,this会被记录为阻塞源,blocker引用会在park结束后自动清除(通过setBlocker(t, null)),避免内存泄漏。
3、parkUntil(Object blocker, long deadline)
阻塞到绝对时间戳(毫秒)唤醒。
4、parkNanos(long nanos)
阻塞指定纳秒时间后自动唤醒。
唤醒类
unpark(Thread thread)
/*** Makes available the permit for the given thread, if it* was not already available. If the thread was blocked on* {@code park} then it will unblock. Otherwise, its next call* to {@code park} is guaranteed not to block. This operation* is not guaranteed to have any effect at all if the given* thread has not been started.** @param thread the thread to unpark, or {@code null}, in which case* this operation has no effect*/
public static void unpark(Thread thread) {if (thread != null) {if (thread.isVirtual()) {VirtualThreads.unpark(thread);} else {U.unpark(thread);}}
}
调用该方法为指定线程提供许可(若该许可尚未可用)。若线程正因调用park而阻塞,则立即解除阻塞;否则保证该线程下一次调用park时不会阻塞(因为已经有许可了)。若指定线程尚未启动,则此操作不保证会产生任何效果。