JDK25记录

张开发
2026/4/3 21:58:52 15 分钟阅读
JDK25记录
JDK25记录虚拟线程介绍使用Scoped Value介绍使用ThreadLocal VS ScopedValue虚拟线程在 JDK 21 之前Java 的线程模型是一个 Java 线程 一个操作系统线程称为平台线程。这种模型存在两个核心问题线程创建成本高创建一个操作系统线程需要分配栈内存通常 1MB 左右并进行内核级资源分配。因此线程池的大小通常被限制在几千个以内。上下文切换开销大当大量线程被阻塞如等待 I/O时操作系统需要频繁进行线程调度导致 CPU 开销增加。为了更好的解决上述两个问题、虚拟线程出现了。介绍虚拟线程将应用中的线程和操作系统隔离开、不再是一对一的关系而是一对多由jvm自己内部初始化一个cpu核心数的线程池来处理众多应用的虚拟线程。调度关系反转了不再像之前线程的执行完全有操作系统调度虚拟线程的调度完全依靠jvm将虚拟线程挂载空闲的载体线程。虚拟线程Virtual Thread虚拟线程是 JVM 管理的轻量级用户态线程。它与操作系统线程的映射关系是 M:N多个虚拟线程运行在少量操作系统线程上。虚拟线程由 JVM 调度而不是操作系统内核。它的创建、阻塞、唤醒都由 Java 运行时处理不涉及内核调用因此成本极低。虚拟线程的栈容量很小初始只有几百字节随需增长且可以被垃圾回收。因此你可以创建百万级的虚拟线程而不用担心内存耗尽。虚拟线程的阻塞操作不会阻塞操作系统线程。当一个虚拟线程被阻塞如等待 I/O、等待锁时JVM 会把它从载体线程Carrier Thread即实际运行它的操作系统线程上 卸载unmount并让载体线程去执行另一个就绪的虚拟线程。当阻塞结束虚拟线程再被 挂载mount 到某个载体线程上继续执行。这个过程对开发者是完全透明的你只需要像写普通线程那样写代码JVM 会帮你完成非阻塞的底层优化。虚拟线程的工作原理理解虚拟线程的关键是载体线程Carrier Thread 和挂载/卸载Mount/Unmount 机制。载体线程就是传统的平台线程操作系统线程。JVM 会维护一个载体线程池默认数量等于 CPU 核数。调度器JVM 的调度器将就绪的虚拟线程分配给载体线程执行。挂载Mount当一个虚拟线程被分配到一个载体线程上执行时它的栈帧、局部变量等会被复制到载体线程的栈中并执行其代码。卸载Unmount当虚拟线程执行了阻塞操作如 Thread.sleep()、socket.read()、ReentrantLock.lock()时JVM 会检测到并 将虚拟线程从载体线程上卸载。卸载时虚拟线程的栈帧被保存到堆内存中载体线程被释放立即去执行另一个虚拟线程。当阻塞操作完成如数据到达、锁被释放虚拟线程会被重新挂载到某个载体线程上继续执行。因为 卸载/挂载 的开销极低仅仅是内存拷贝所以即使有百万级虚拟线程频繁阻塞载体线程也不会被长时间占用系统吞吐量可以线性增长。使用理论上虚拟线程虽然“海量”可以支持无限并发、但需要考虑下游资源数据库连接池、外部API、文件系统依然是有限的。需要通过主动控制流量速度的策略。Semaphore信号量就像一个通行证管理器你可以限制同时执行的虚拟线程数量超出的任务会阻塞等待直到有可用的“许可”。// 最多同时执行 1000 个虚拟线程SemaphoresemaphorenewSemaphore(1000);try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){for(inti0;i100_000;i){executor.submit(()-{semaphore.acquire();// 获取许可没有则阻塞try{// 业务逻辑数据库查询、外部 API 调用等doBusiness();}finally{semaphore.release();// 释放许可}});}}可以将虚拟线程与传统的有界阻塞队列结合并通过自定义拒绝策略来控制背压// 自定义有界线程池但线程使用虚拟线程工厂intcoreSize10;// 核心线程数intmaxSize1000;// 最大线程数intqueueSize100;// 有界队列ThreadFactoryvirtualThreadFactoryThread.ofVirtual().name(vt-,0).factory();ExecutorServiceexecutornewThreadPoolExecutor(coreSize,maxSize,60,TimeUnit.SECONDS,newLinkedBlockingQueue(queueSize),virtualThreadFactory,newThreadPoolExecutor.CallerRunsPolicy()// 背压策略);Scoped Value介绍从JDK 21 开始引入了虚拟线程传统的 ThreadLocal 开始暴露出问题。ScopedValue 正是为了解决这些问题而诞生的——它是一个不可变的、作用域绑定的上下文值传递机制在 JDK 25 中已正式发布JEP 506从 JDK 21 预览到 JDK 25 最终确定。ThreadLocal 的问题内存泄漏风险虚拟线程数量可能达到百万级如果每个虚拟线程都持有 ThreadLocal 变量内存会迅速耗尽。生命周期不明确ThreadLocal 的值在线程生命周期内始终存在需要手动 remove()容易被遗忘。可修改性ThreadLocal 是可变的任何代码都能修改它导致难以追踪的数据篡改。父子线程传递困难虚拟线程通常使用 Executors.newVirtualThreadPerTaskExecutor() 创建父线程的 ThreadLocal 不会自动传递给子线程。性能开销ThreadLocal 的查找在大量线程下有一定开销。使用特性说明不可变性一旦绑定不能修改避免数据竞态作用域绑定只在特定的代码块run 方法内有效自动清理离开作用域后值自动销毁无需手动 remove虚拟线程友好在虚拟线程阻塞、卸载、挂载时都能正确保持结构化并发集成与 StructuredTaskScope 无缝配合自动传递给子任务高性能实现基于轻量级上下文比 ThreadLocal 更快、内存占用更少ScopedValue.where(RequestContext.CONTEXT, info).run(() - { })ComponentpublicclassScopedValueFilterimplementsFilter{OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain){RequestInfoinfoextractInfo(request);ScopedValue.where(RequestContext.CONTEXT,info).run(()-{chain.doFilter(request,response);});}}ThreadLocal VS ScopedValue生命周期 - 两者最直观的区别ThreadLocal值绑定在线程上随线程创建而存在随线程销毁而消失或手动 remove。在线程的整个生命周期内任何代码都可以读写它。ScopedValue值绑定在代码块作用域上进入作用域时绑定退出作用域时自动销毁。作用域内的代码可以读取但不能修改。不过除了生命周期还有两个重要差异值得注意可变性ThreadLocal 可以随时 set 新值是可变的ScopedValue 一旦绑定就不可变只能通过嵌套新作用域来“覆盖”但原值不变。传递性ThreadLocal 默认不传递给子线程除非用 InheritableThreadLocal但有性能问题ScopedValue 与结构化并发StructuredTaskScope天然集成子任务会自动继承父作用域的值更适合虚拟线程场景。

更多文章