JUC并发编程:LockSupport.park() 与 unpark() 深度解析

一、前言

在Java并发编程中,AQS (AbstractQueuedSynchronizer) 是实现锁(如 ReentrantLock)、同步器(如 CountDownLatch)的核心基础。而 AQS 能够实现线程的阻塞与唤醒,其底层完全依赖于 LockSupport 工具类。

LockSupport 是 JUC 包中的一个工具类,主要用于挂起(block)和唤醒(wake up)线程。它所有的方法都是静态的,底层通过 Unsafe 类实现。


二、基本使用

LockSupport 主要是通过 park() 和 unpark(Thread thread) 来实现的。

  • park(): 阻塞当前线程,直到获得"许可"或被中断。

  • unpark(Thread thread): 给指定线程发放一个"许可",如果该线程被阻塞,则将其唤醒。

1. 标准用法:先 park 后 unpark

这是最直观的用法,主线程让子线程阻塞,然后三秒后唤醒它。

java 复制代码
import java.util.concurrent.locks.LockSupport;

public class ParkUnparkDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " --- 1. 开始运行");
            // 阻塞当前线程
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " --- 3. 被唤醒");
        }, "t1");

        t1.start();

        // 睡眠3秒,确保t1先执行并进入park状态
        Thread.sleep(3000);
        
        System.out.println(Thread.currentThread().getName() + " --- 2. 发起唤醒");
        // 唤醒线程t1
        LockSupport.unpark(t1);
    }
}

运行结果:

codeText

复制代码
t1 --- 1. 开始运行
main --- 2. 发起唤醒
t1 --- 3. 被唤醒

2. 特殊用法:先 unpark 后 park

这是 LockSupport 与 Object.wait() 最大的区别之一。

即使 unpark 操作发生在 park 之前,线程依然可以正常通过(不会永久阻塞)。

java 复制代码
import java.util.concurrent.locks.LockSupport;

public class UnparkFirstDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                // 让子线程睡一会儿,保证主线程先执行 unpark
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " --- 2. 准备park");
            // 因为之前已经有许可了,这里不会阻塞,直接通过
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " --- 3. park结束,继续执行");
        }, "t1");

        t1.start();
        
        System.out.println(Thread.currentThread().getName() + " --- 1. 提前发放许可");
        // 提前给t1线程发放许可
        LockSupport.unpark(t1);
    }
}

原理简述:unpark 就像是给线程的钱包里预存了一张票,等线程执行 park 需要买票进站时,发现钱包里已经有票了,直接刷票进站,无需等待。


三、与 Object.wait/notify 的区别(重点)

在面试和实际开发中,这两组机制经常被拿来对比:

|----------|----------------------------------------------------------|-------------------------------------------------|
| 特性 | Object.wait() / notify() | LockSupport.park() / unpark() |
| 所属对象 | Object 类的方法 | LockSupport 类的静态方法 |
| 锁的要求 | 必须在 synchronized 代码块中执行,否则抛 IllegalMonitorStateException | 不需要获取任何锁,随时随地可调用 |
| 锁的释放 | 执行 wait 会释放当前持有的锁 | 执行 park 不会释放当前持有的锁(容易死锁,需注意) |
| 执行顺序 | 必须 先 wait 后 notify。如果先 notify 后 wait,线程会永久等待 | 顺序无关。可以先 unpark 存入许可,后 park 消费许可 |
| 中断响应 | 抛出 InterruptedException 异常 | 不抛异常,park 方法直接返回,需要通过 Thread.interrupted() 检查状态 |


四、底层原理深度解析

LockSupport 的底层是一个 native 方法,最终调用的是 JVM 内部(C++实现)的 Parker 对象。核心理念是 "Permit"(许可)

1. Permit (许可) 机制

我们可以将 Permit 理解为一个二元信号量 (0 或 1),它不累加

  • park() 流程:

    1. 检查当前线程是否有许可(Permit)。

    2. 如果有(Permit = 1):消耗许可(Permit 变为 0),park 方法立即返回,线程继续运行。

    3. 如果没有(Permit = 0):阻塞当前线程,直到被唤醒。

  • unpark(Thread t) 流程:

    1. 将指定线程的许可设置为 1(Permit 变为 1)。

    2. 注意: 因为是二元信号量,多次调用 unpark 不会叠加。unpark 100次,Permit 依然是 1。此时调用一次 park 就会消耗掉这个许可,第二次调用 park 依然会阻塞。

2. 源码级验证 (HotSpot VM)

在 JVM 源码(os_linux.cpp 和 os_windows.cpp 等)中,每个 Java 线程都有一个对应的 C++ 类 Parker 实例。

Parker 类主要包含三个核心变量:

  1. _counter: 记录许可数量(0 或 1)。

  2. _mutex: 互斥锁(用于保护操作)。

  3. _cond: 条件变量(用于线程挂起)。

Java底层图示:
C++ 伪代码逻辑演示

park() 的大致逻辑:

cpp 复制代码
void Parker::park(bool isAbsolute, jlong time) {
    // 1. 如果已有许可(_counter > 0),直接消耗许可并返回
    if (_counter > 0) {
        _counter = 0;
        return;
    }

    // 2. 获取互斥锁
    pthread_mutex_lock(_mutex);
    
    // 3. 再次检查许可(防止并发情况)
    if (_counter > 0) {
        _counter = 0;
        pthread_mutex_unlock(_mutex);
        return;
    }

    // 4. 真正阻塞,等待信号(类似Java的wait)
    // 这里会释放锁并等待,直到被unpark唤醒或中断
    pthread_cond_wait(_cond, _mutex);
    
    // 5. 唤醒后,将许可归零
    _counter = 0;
    pthread_mutex_unlock(_mutex);
}

unpark() 的大致逻辑:

java 复制代码
void Parker::unpark(Thread* thread) {
    // 1. 获取互斥锁
    pthread_mutex_lock(_mutex);
    
    // 2. 只有当许可为0时,才设置为1(不可累加)
    // 但无论之前是多少,最终状态都是1
    int s = _counter;
    _counter = 1;
    
    // 3. 如果线程之前是阻塞状态(s < 1),唤醒它
    if (s < 1) {
        // 发送信号唤醒在 _cond 上等待的线程
        pthread_cond_signal(_cond);
    }
    
    pthread_mutex_unlock(_mutex);
}

五、常见面试题与坑

1. 为什么推荐使用 park(Object blocker)?

在 Java 6 之后,LockSupport 新增了 park(Object blocker) 方法。

  • blocker 是用来标识当前线程在等待什么对象(通常传入 this)。

  • 作用: 当使用 jstack 或 Profiler 工具排查死锁或性能问题时,能够显示出线程是被哪个对象 block 住的,极大地方便了问题定位。

对比:

  • park() 在 dump 文件中显示:parking to wait for <0x0000000000000000> (无信息)

  • park(this) 在 dump 文件中显示:parking to wait for <0x000000076b3820f0> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

2. 中断(Interrupt)能唤醒 park 吗?

能。

当一个线程被 park 阻塞时,如果其他线程调用了该线程的 interrupt() 方法,park 会立即返回。
注意:不会抛出 InterruptedException,你需要手动检查中断标志位。

java 复制代码
Thread t1 = new Thread(() -> {
    LockSupport.park();
    // 这里的代码会在 unpark 或 interrupt 后执行
    if (Thread.currentThread().isInterrupted()) {
        System.out.println("被中断唤醒了");
    }
});
t1.start();
t1.interrupt(); // 线程会立刻从 park 中醒来

3. 多次 unpark,能否抵消多次 park?

不能。

java 复制代码
LockSupport.unpark(Thread.currentThread());
LockSupport.unpark(Thread.currentThread()); // 此时 permit 还是 1

LockSupport.park(); // 消耗 permit (1 -> 0),通过
LockSupport.park(); // permit 为 0,阻塞!

六、总结

  1. LockSupport 是 JUC 并发包的基石,AQS 依靠它来挂起和唤醒线程。

  2. 它利用 Permit(许可) 机制,解耦了阻塞和唤醒的顺序限制(可以先唤醒后阻塞)。

  3. 它底层利用操作系统的 Mutex 和 Condition Variable 实现,轻量且高效。

  4. 在使用时,要注意它不释放锁的特性,避免造成死锁。

相关推荐
Remember_9931 小时前
【数据结构】二叉树:从基础到应用全面解析
java·数据结构·b树·算法·leetcode·链表
冷冷的菜哥1 小时前
springboot调用ffmpeg实现对视频的截图,截取与水印
java·spring boot·ffmpeg·音视频·水印·截图·截取
2501_940315261 小时前
蓝桥云课:分巧克力(二分查找法)
数据结构·c++·算法
灵犀坠1 小时前
Vue3 实现音乐播放器歌词功能:解析、匹配、滚动一站式教程
开发语言·前端·javascript·vue.js
人工智能AI技术1 小时前
Java程序员如何入门AI
java·人工智能
行稳方能走远1 小时前
Android C++ 学习笔记5
android·c++
乌萨奇也要立志学C++1 小时前
【Linux】信号量 信号量详解与应用和基于环形队列实现单 / 多生产消费模型
linux·c++
我是小疯子661 小时前
C++图论:从基础到实战应用
java·c++·图论
小码过河.1 小时前
设计模式——享元模式
java·设计模式·享元模式