Day44 | J.U.C中的LockSupport详解

在之前的文章中,我们已经熟悉了Java并发包中的原子类。今天,我们将继续深入J.U.C的核心,探讨一个看着不太起眼但实际却很重要的底层工具------LockSupport。

LockSupport是java.util.concurrent包里线程阻塞和唤醒机制的基础。

如果我们把ReentrantLock、Semaphore等高级同步工具看成精密仪器,那LockSupport就是制造这些仪器所依赖的最核心的零件之一。

理解他,便于理解我们之后将要学习的AQS (AbstractQueuedSynchronizer)。

一、为什么需要LockSupport?

在LockSupport出现之前,我们有两种主要的方式来让线程等待和唤醒:

1、使用Thread.suspend()和Thread.resume()

这对方法已经被废弃。因为太容易导致死锁。如果一个持有锁的线程被suspend(),他就永远不会释放锁,导致其他等待这个锁的线程无限期地阻塞。

2、使用Object.wait()和Object.notify()/notifyAll()

这是最经典和常用的方式。但是这种方式也有一定程度的限制。

比如必须在synchronized块里调用,wait()和notify()必须跟对象的监视器锁绑定,这就意味着线程必须先获取锁,才能进行等待或通知操作,不够灵活。

还会存在信号丢失的问题,比如一个线程先于另一个线程调用notify(),之后另一个线程才调用wait(),那么这个通知信号就丢失了,wait()的线程会无限等待下去。

二、LockSupport的核心机制

LockSupport给每个线程都关联了一个许可(Permit)。这个许可像一张通行证,他是一个二进制的信号量(要么有,要么没有)。

LockSupport最核心的两个方法就是围绕这个许可工作的:

park方法

java 复制代码
public static void park() {
        U.park(false, 0L);
    }

如果当前线程持有许可,那么他会消耗掉这个许可,并立即返回,线程继续运行。

如果当前线程没有许可,那么他就会阻塞,直到有其他线程给他发放许可。

unpark方法

java 复制代码
public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }

给一个指定的线程发放一个许可。

如果thread当前正因为park()而阻塞,他会被立刻唤醒。

如果thread当前没有阻塞,那么这次发放的许可就会被攒起来(不是累加,最多一个)。当这个线程下一次调用park()的时候,他会立刻消耗掉这个许可,从而避免阻塞。

unpark()可以先于park()调用,并且许可不会丢失。这就解决了wait/notify的信号丢失问题。

当我们打开LockSupport的源码,其实会发现代码量并不多:

其中重要的就是

添加图片注释,不超过 140 字(可选)

Unsafe被称之为Java的上帝之手,是因为他可以执行内存操作等一些底层操作。

parkBlocker是Thread类的一个字段,用来调试和监控,记录当前线程是被哪个对象所阻塞的。LockSupport.park(Object blocker)就会设置这个字段。

UNSAFE.park()和UNSAFE.unpark()这两个native方法,实现都在JVM内部(C/C++编写),最终会调用操作系统的线程调度原语,实现真正意义上的线程挂起和恢复。

三、LockSupport实战

概念和术语通常都会让人摸不着头脑,惯例还是直接上手写一下,接下来我们看一下两个LockSupport的代码示例:

1、park与unpark方法的使用

java 复制代码
package com.lazy.snail.day44;

import java.util.concurrent.locks.LockSupport;

/**
 * @ClassName LockSupportDemo1
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/9/1 13:56
 * @Version 1.0
 */
public class LockSupportDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": 即将调用park()进入等待...");
            // 这里线程还没有许可,线程会阻塞
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + ": 已被unpark,继续执行。");
        }, "工人线程");

        worker.start();

        System.out.println("主线程休眠2秒,准备为工人线程发放许可...");
        Thread.sleep(2000);

        System.out.println("主线程调用unpark() ...");
        // 给worker线程发放一个许可
        LockSupport.unpark(worker);
    }
}

示例代码中主线程创建并启动了worker线程。

worker打印输出后,执行LockSupport.park()。由于没有许可,worker阻塞,线程状态变成WAITING。

主线程sleep(2000),保证worker已经真的阻塞在park()。

然后主线程调用LockSupport.unpark(worker),给worker发放了一个许可,如果线程正阻塞在park(),会被唤醒并消费掉许可(许可回到0)。

worker从park()返回,打印输出后线程结束。

这段代码演示的就是LockSupport最核心的一次性许可机制,park()消费许可进入/退出等待,unpark(thread)给指定线程发放最多1个许可。

2、unpark先于park执行

之前我们说wait/notify会有信号丢失的问题,通过下面的案例来看看LockSupport存不存在这个问题。

java 复制代码
package com.lazy.snail.day44;

import java.util.concurrent.locks.LockSupport;

/**
 * @ClassName LockSupportDemo2
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/9/1 14:43
 * @Version 1.0
 */
public class LockSupportDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + ":等待2秒...");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":即将调用park()...");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + ":park()执行完毕,没有阻塞。");
        }, "工人线程");
        worker.start();
        System.out.println("主线程提前给工人线程发放许可...");
        LockSupport.unpark(worker);
    }
}

示例代码中主线程启动worker,然后马上unpark(worker),许可被置成了1。

worker线程sleep(2s)醒来后调用park(),发现许可是1,马上返回并消费。

worker线程继续执行并结束,全程不阻塞。

这个示例说明了先unpark,后park也不会阻塞。

3、park()对线程中断的响应

当一个被park()阻塞的线程被interrupt()时,park()方法会立即返回,并且该线程的中断状态会被设置为true。

复制代码
java 复制代码
package com.lazy.snail.day44;

import java.util.concurrent.locks.LockSupport;

/**
 * @ClassName LockSupportDemo3
 * @Description TODO
 * @Author lazysnail
 * @Date 2025/9/1 15:59
 * @Version 1.0
 */
public class LockSupportDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("工人线程:即将park...");
            LockSupport.park();
            System.out.println("工人线程:已被唤醒。");
            System.out.println("工人线程中断状态:" + Thread.currentThread().isInterrupted());
        });

        worker.start();
        Thread.sleep(2000);

        System.out.println("主线程:中断工人线程...");
        worker.interrupt();
    }
}

示例代码中,worker线程被阻塞,然后在主线程中被中断,park方法就马上返回了,中断状态变成了true。

四、核心属性及方法

核心属性

java 复制代码
private static final Unsafe U = Unsafe.getUnsafe();
    private static final long PARKBLOCKER
        = U.objectFieldOffset(Thread.class, "parkBlocker");
    private static final long TID
        = U.objectFieldOffset(Thread.class, "tid");

**U:**Unsafe类的一个实例,LockSupport使用他来调用底层的原生park和unpark函数。

**PARKBLOCKER:**一个long类型的值,他保存了Thread对象内部parkBlocker字段的内存偏移量。parkBlocker是一个对象,可以跟一个线程关联,来表示他被阻塞的原因。

**TID:**也是一个long类型的值,存储了Thread对象内部tid(线程ID)字段的内存偏移量。这样LockSupport就可以直接从内存里拿到线程的ID。

核心方法

java 复制代码
public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }

public static void park() {
        U.park(false, 0L);
    }

public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);
        setBlocker(t, null);
    }

unpark(Thread thread)、park()和park(Object blocker)已经在第三节中做了介绍。

**parkNanos(long nanos)**方法是带超时的park(),表示最多阻塞nanos纳秒。

**parkUntil(long deadline)**方法是带截止日期的park(),表示最多阻塞到deadline这个绝对时间点。

**getBlocker(Thread t)**用来获取指定线程t的blocker对象,主要用于监控和诊断。

五、LockSupport和AQS的关系

LockSupport实际是AQS框架的线程调度引擎。

AQS内部维护了一个等待线程的队列。

当一个线程尝试获取锁失败后,AQS会把他包装成一个节点(Node)加入等待队列,然后调用 LockSupport.park(this)把这个线程安全地挂起。

当持有锁的线程释放锁时,AQS会从队列中找到需要被唤醒的后继节点,然后调用 LockSupport.unpark(node.thread) 来精确地唤醒那个等待的线程。

结语

通过阅读本文,以及前期讲的Thread.sleep(ms)、Object.wait()、Condition.await(),我把这些线程阻塞的方法列一个表,方便理解对别:

| 特性 / 方法 | Thread.sleep(ms) | Object.wait() | Condition.await() | LockSupport.park() |
| 是否释放锁 | 不释放 | 释放synchronized锁 | 释放Lock锁 | 不释放 |
| 调用要求 | 无要求 | 必须在synchronized块中 | 必须持有Lock | 无要求 |
| 唤醒方式 | 时间到期 | notify/notifyAll | signal/signalAll | unpark/interrupt |
| 响应中断 | 抛出InterruptedException | 抛出InterruptedException | 抛出InterruptedException | 不抛异常,直接返回并设置中断状态 |
| 信号丢失问题 | 不适用 | 会丢失 (先notify后wait) | 会丢失 (先signal后await) | 不会丢失 (先unpark后park) |

底层实现 JVM/OS 对象监视器(monitor) AQS + LockSupport.park Unsafe + OS线程调度原语

下一篇预告

Day45 | J.U.C中AQS的完全指南(上)

如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!

相关推荐
闲人编程3 小时前
Python设计模式实战:用Pythonic的方式实现单例、工厂模式
开发语言·python·单例模式·设计模式·工厂模式·codecapsule·pythonic
Mintopia3 小时前
🧠 Next.js 还是 Nuxt.js?——当 JavaScript 碰上命运的分叉路
前端·后端·全栈
5pace3 小时前
Mac Nginx安装、启动、简单命令(苍穹外卖、黑马点评前端环境搭建)
java·前端·nginx·macos·tomcat
Moniane3 小时前
API技术深度解析:从基础原理到最佳实践
开发语言
库库8393 小时前
Spring AI 知识点总结
java·人工智能·spring
扶苏-su4 小时前
Java---String类
java·开发语言·string类
nexttake4 小时前
5.go-zero集成gorm 和 go-redis
开发语言·后端·golang
我叫张土豆4 小时前
Neo4j 版本选型与 Java 技术栈深度解析:Spring Data Neo4j vs Java Driver,如何抉择?
java·人工智能·spring·neo4j
Victor3564 小时前
Redis(84)如何解决Redis的缓存击穿问题?
后端