当前位置:首页 > 文章列表 > 文章 > java教程 > Java 虚拟线程为何可能被 synchronized 固定

Java 虚拟线程为何可能被 synchronized 固定

来源:17golang原创 2026-11-14 22:39:01 0浏览 收藏

Java 虚拟线程并不是一遇到 synchronized 就一定会被“固定”在 carrier 线程上。这个说法主要来自 JDK 21–23 的实现限制:虚拟线程在 monitor 保护区内遇到某些阻塞操作时,可能无法从 carrier 卸载,长时间、频繁发生就会拖慢整体吞吐。JDK 24 已通过 JEP 491 改善 synchronized 的阻塞处理,但 native 方法和 Foreign Function & Memory API 仍可能造成 pinning。

官方资料:https://docs.oracle.com/en/java/javase/26/core/virtual-threads.html

实际排查时先看运行时版本和 JFR 事件,再决定是否改锁。JDK 24 及以后不需要因为“虚拟线程 + synchronized”这个组合本身就全面替换锁;真正应该优先修的是长时间持锁、锁内阻塞 I/O,以及仍会让虚拟线程无法卸载的 native/foreign 调用。

先把“被固定”理解成 carrier 没有被释放

虚拟线程由 Java 运行时调度,执行时会暂时挂载到一个平台线程上,这个平台线程就是 carrier。普通的阻塞 I/O 通常可以让虚拟线程挂起并释放 carrier,其他虚拟线程随后可以使用这个 carrier。pinning 的关键不是虚拟线程停止工作,而是它在阻塞时继续占着底层平台线程。

synchronized 使用对象 monitor 提供互斥和 happens-before 保证。旧版本中,如果虚拟线程在 synchronized 方法或代码块内执行阻塞操作,或者等待一个竞争激烈的 monitor,carrier 可能被一并占住。一个短小的内存临界区通常不会造成可感知问题;真正危险的是把网络、文件、数据库或外部 SDK 调用包在锁里。

虚拟线程、carrier、synchronized monitor 与 native 调用的固定边界说明图
图1:虚拟线程与 carrier、monitor、阻塞操作的静态关系;这是版本边界说明图,不是运行截图或执行证据。

先确认你面对的是哪个 JDK 行为

如果线上服务运行在 JDK 21、22 或 23,历史文章中关于 synchronized pinning 的提醒仍然有参考价值,尤其要关注频繁进入、并且会等待 I/O 的临界区。Oracle 的 JDK 23 指南明确建议避免频繁且长时间的 pinning,并在确有问题时考虑 ReentrantLock。

JDK 24 引入“Synchronize Virtual Threads without Pinning”。Oracle 的 JDK 26 迁移指南说明,JDK 24 会让在 synchronized 方法或语句中阻塞的虚拟线程释放底层平台线程,交给其他虚拟线程使用。因此,升级到 JDK 24+ 后,不能再简单地把所有 synchronized 都判定为 pinning 来源。

运行时范围需要重点关注的现象处理判断
JDK 21–23synchronized 内的长阻塞、竞争 monitor 可能拖住 carrier优先缩短临界区,并对高频长阻塞点评估 ReentrantLock
JDK 24+synchronized 的阻塞可更好地释放 carrier,但 native/foreign 阻塞仍可能 pin保留正确的短临界区,使用 JFR 找实际事件
所有版本锁内做外部调用会放大排队和尾延迟先重构资源边界,不要只换一个锁类名

最先改的是锁内的外部调用

下面的旧式写法把远程读取放进了 monitor。即使当前 JDK 能处理一部分 synchronized 阻塞,这种结构仍然让所有竞争者共享一个长临界区:远程服务变慢时,内存状态更新也会跟着排队。

final class ProfileCache {
    private final Object lock = new Object();
    private Profile profile;

    Profile load(String userId) throws IOException {
        synchronized (lock) {
            // 远程 I/O 的时长不可控,不要让它占着共享 monitor。
            Profile loaded = fetchFromService(userId);
            profile = loaded;
            return loaded;
        }
    }

    private Profile fetchFromService(String userId) throws IOException {
        // 示例只表达边界:真实项目里这里可能是 HTTP、JDBC 或文件读取。
        return remoteClient.fetch(userId);
    }
}

更稳妥的拆法是先在锁外取得数据,再在锁内完成短小的状态合并。若业务要求“只有一个请求负责刷新”,还要额外设计刷新标记或使用更高层的缓存协调机制,不能因为移出 synchronized 就默认消除了重复请求。

final class ProfileCache {
    private final Object lock = new Object();
    private Profile profile;

    Profile load(String userId) throws IOException {
        // 先在锁外执行可能长时间阻塞的调用。
        Profile loaded = fetchFromService(userId);
        synchronized (lock) {
            // 锁内只做内存状态更新,保持临界区短小。
            profile = loaded;
            return profile;
        }
    }

    private Profile fetchFromService(String userId) throws IOException {
        // 统一保留底层异常,让调用方决定重试或降级。
        return remoteClient.fetch(userId);
    }
}

需要可中断或超时时,再考虑 ReentrantLock

ReentrantLock 不是虚拟线程的“自动解 pin 开关”,它的价值在于提供更细的控制:可以尝试加锁、响应中断、设置超时或选择公平策略。只有当这些能力确实解决了你的调度需求,或者在旧 JDK 上定位到长时间、频繁的 synchronized pinning 时,替换才有意义。

final class RefreshGate {
    private final ReentrantLock lock = new ReentrantLock();

    boolean tryRefresh(Duration timeout) throws InterruptedException {
        // 超时让调用方可以放弃等待,而不是无限占用请求资源。
        if (!lock.tryLock(timeout.toNanos(), TimeUnit.NANOSECONDS)) {
            return false;
        }
        try {
            // 这里应放短小的共享状态操作;不要把远程 I/O 放进锁内。
            refreshInMemoryState();
            return true;
        } finally {
            // 无论刷新成功还是抛出异常,都必须释放锁。
            lock.unlock();
        }
    }
}

如果只是保护一个很短的字段读写,synchronized 更直接,也更容易让维护者看出互斥范围。换锁时要同时复核可见性、异常路径和锁对象是否保持一致,不能只做机械替换。

synchronized、ReentrantLock、Semaphore 与外部 I/O 边界的方案关系图
图2:把共享状态、锁控制、并发上限和外部 I/O 分开考虑的静态方案关系图,不代表某次程序运行结果。

并发上限应该用 Semaphore 表达

很多团队用固定平台线程池限制外部服务并发,迁移到虚拟线程后仍然保留同一个池,结果把“线程资源管理”和“业务并发上限”混在了一起。Oracle 的虚拟线程采用指南建议用 Semaphore 表达受限资源的并发数;数据库连接池本身也已经承担了类似的排队职责。

final class LimitedClient {
    private final Semaphore permits = new Semaphore(10);

    Response call(Request request) throws InterruptedException {
        // 信号量表达的是外部服务最多允许的并发请求数。
        permits.acquire();
        try {
            // 虚拟线程可以在这里等待;不要为每个任务再建一个固定线程池。
            return client.send(request);
        } finally {
            // 无论远程调用成功、失败还是被取消,都归还许可。
            permits.release();
        }
    }
}

用 JFR 找仍然存在的真实 pinning

不要仅凭代码中出现了 synchronized 就判断服务一定被 pinning。JDK 文档提供了 jdk.VirtualThreadPinned 事件,它默认在阻塞超过阈值时记录;JDK 26 虚拟线程文档还说明,native 方法或 foreign function 是当前仍需重点关注的 pinning 来源。

# 启动一次录制,并在 JVM 退出时把 JFR 文件写到指定位置。
java -XX:StartFlightRecording:dumponexit=true,filename=virtual-thread.jfr \
     -cp app.jar com.example.Main

# 只打印虚拟线程被固定的事件,定位阻塞操作和调用栈。
jfr print --events jdk.VirtualThreadPinned virtual-thread.jfr

看到事件后,先看 blockingOperation、pinnedReason 和调用栈,再回到代码判断是 monitor、native/foreign 调用,还是其他运行时边界。没有事件时,不要为了迎合旧文章而全面替换 synchronized。

最后用这张表做选择

场景优先方案判断依据
短小的内存状态互斥synchronized语义清楚,临界区短,不涉及外部阻塞
需要超时、可中断或公平策略ReentrantLock需要显式控制加锁和释放路径
限制外部服务或资源并发Semaphore表达的是许可数量,不是平台线程数量
native/foreign 调用或锁内长 I/O重构边界并监控先缩短阻塞范围,再用 JFR 观察事件

所以,“Java 虚拟线程为何可能被 synchronized 固定”的准确答案是:旧 JDK 的 monitor 阻塞曾经让 carrier 无法释放,JDK 24 起这部分实现已经改善;现在更应该关注 native/foreign 阻塞和锁内长耗时操作。选择同步工具时,先看临界区、等待策略和资源上限,再看锁的名字。

相关问题

  • JDK 24 以后还需要把 synchronized 全部改成 ReentrantLock 吗?
  • 虚拟线程里用固定线程池限制并发有什么问题?
  • JFR 的 jdk.VirtualThreadPinned 事件应该重点看哪些字段?
  • 为什么把网络请求放到 synchronized 外面仍可能产生重复刷新?
版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Java ModuleDescriptor.Version 怎样比较模块版本Java ModuleDescriptor.Version 怎样比较模块版本
上一篇
Java ModuleDescriptor.Version 怎样比较模块版本
Java Character.isEmoji 怎样按 Unicode 码点判断 Emoji
下一篇
Java Character.isEmoji 怎样按 Unicode 码点判断 Emoji
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    1029次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1030次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1173次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1133次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    973次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码