并发编程原理与实战(二十四)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)) {
        // 阻塞时记录当前锁实例为blocker
        LockSupport.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时不会阻塞(因为已经有许可了)。若指定线程尚未启动,则此操作不保证会产生任何效果。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值