news 2026/6/15 12:12:49

Java 并发 100 问:从面试到生产(三)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 并发 100 问:从面试到生产(三)

17. java项目中,如何判断一段代码是否有线程安全问题

第一步:寻找“共享可变状态”(核心判断标准)

线程安全问题产生的三个必要条件(缺一不可):

判断结论

  • • 如果代码是单线程运行的,线程安全。

  • • 如果代码没有共享变量(全部是局部变量、方法参数),绝对线程安全

  • • 如果有共享变量,但是它是不可变的(共享变量是引用类型的,需要满足引用不可变也就是final修饰和对象不可变;是基本类型的,那么只要final修饰即可),绝对线程安全

  • • 如果有共享且可变的变量,存在线程安全隐患,进入第二步

第二步:基于 JMM(Java内存模型)三大特性分析

如果存在“共享可变状态”,就需要检查是否破坏了以下三个特性:

1. 原子性
  • 含义:一个操作或多个操作,要么全部执行且执行过程不被中断,要么都不执行。

  • 典型反例i++i--a = a + 1。这些操作在字节码层面是“读-改-写”三步,非原子操作。

  • 复合操作:如if (map.contains(key)) { map.put(key, value); },即使containsput自身是线程安全的(如ConcurrentHashMap),组合在一起也不是原子的。

  • 判断点:对共享变量的修改,是否是一个非原子的复合操作?

2. 可见性
  • 含义:一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

  • 典型反例:线程A将变量flag=false修改为true,线程B一直在while(!flag)循环中,可能永远读不到true,因为B读取的是自己工作内存中的旧值。

  • 判断点:共享变量是否被volatile修饰?对变量的读写是否在synchronized块或Lock中?

3. 有序性
  • 含义:程序执行的顺序按照代码的先后顺序。

  • 典型反例:指令重排。最经典的是单例模式的“双重检查锁(DCL)”

  • 判断点:是否存在没有适当同步保障的“先检查后执行”逻辑?共享变量是否加volatile禁止重排?

总结:排查清单

当你拿到一段代码,请按以下清单过一遍:

  1. 有没有多线程访问?(Web接口、线程池、定时任务、消息消费者等默认都是多线程)
  2. 有没有共享变量?(全局变量、静态变量、单例对象中的成员变量)
  3. 共享变量会被修改吗?(只读是安全的)
  4. 修改操作是原子的吗?(i++不是,AtomicInteger是)
  5. 修改后的值对其他线程立即可见吗?(没加volatile或没在锁内可能不可见)
  6. 代码逻辑是否依赖前一个状态?(检查再执行逻辑,必须整体加锁,防止指令重排)
  7. 使用的JDK类是线程安全的吗?(HashMapConcurrentHashMapSimpleDateFormatDateTimeFormatter或用ThreadLocal

18. 为什么现代Java项目,不推荐使用Timer实现定时任务?

Timer的核心缺陷如下:

  1. 单线程阻塞问题
    由于Timer只有一个TimerThread,如果其中一个TimerTask执行时间过长,或者因为某些原因卡住了,其他所有任务都会被阻塞,导致后续任务的执行时间延后,丧失定时准确性。

  2. 单点故障问题
    如果任何一个TimerTask抛出了未捕获的异常(如RuntimeException),TimerThread会因为异常而退出并死亡,整个Timer就会崩溃。队列中所有尚未执行的任务都会被静默丢弃,且不会再接受新任务,这在生产环境中是灾难性的。

  3. 对系统时钟敏感
    Timer依赖的是System.currentTimeMillis()(绝对时间)。如果在运行期间操作系统的时钟被回拨,Timer会出现混乱,可能导致任务被疯狂执行或长时间挂起。

如果老项目中,一个Timer对应一个TimerTask(没有单线程阻塞问题),每个TimerTask间隔一天执行一次,每个TimerTask都做了兜底的异常处理(没有单点故障问题),虽然仍会造成线程资源浪费,如果运行稳定,也可以维持现状。

19. Go 语言的协程(轻量级线程),java中是否有类似实现,为什么要设计它?

Java 语言里,原有的Thread是和操作系统线程一一对应的,从JDK 21开始支持的虚拟线程(Virtual Threads,Project Loom),则是一种轻量级线程,从此java也有了对协程的原生支持。

为什么要设计这种“轻量级线程”(虚拟线程)?

OS 线程的三大痛点
  1. 资源开销大
  • • 栈默认 1–2MB,几千个线程就能把内存吃光。

  • • 创建/销毁要走系统调用,涉及内核对象、页表等,开销在毫秒级。

  1. 上下文切换代价高
  • • 每次切换都要:用户态 → 内核态 → 保存/恢复寄存器、栈、TLB 等 → 用户态。

  • • 线程多了,CPU 时间大量浪费在切换上。

  1. I/O 阻塞 = 线程浪费
  • • 典型 Web/微服务场景:一个请求一个线程,线程大部分时间在等 DB/HTTP。

  • • 等的时候线程啥也不干,但 OS 线程是宝贵资源,结果就是:

  • • 并发上不去(几千线程就顶天),

  • • CPU 利用率低(线程都在等 I/O)。

为什么不用“异步非阻塞 + 回调”?

为了解决 I/O 阻塞问题,Java 世界早有异步方案(NIO、Netty、CompletableFuture、Reactor):

  • • 优点:

  • • 少量线程就能处理大量连接(事件循环 / epoll)。

  • • 缺点:

  • • 代码变成回调链或响应式流,可读性/可维护性差,调试难。

  • • 业务逻辑被拆得稀碎,错误处理、上下文传递都很痛苦。

Go 的思路是:我不想写回调,也不想管线程池,我就想“一个请求一个 goroutine,同步写到底”。虚拟线程的思路也一样:让“每请求一线程”的模型在高并发下也能用

虚拟线程如何解决这些问题?
  1. 用户态调度,避免内核切换
  • • JVM 自己维护调度器,在用户态切换 虚拟线程。

  • • 切换只是保存/恢复少量寄存器、栈指针,不陷入内核,开销极小。

  1. 极轻量栈,海量并发
  • • 虚拟线程:初始几百字节~几 KB,按需增长。

  • • 同样 8GB 内存,只能装几千 OS 线程,但可以装几十万/百万虚拟线程。

  1. 阻塞不浪费载体线程
  • • 虚拟线程阻塞时,JVM 把它从载体线程卸载,载体线程继续跑其他虚拟线程。

  • • 结果:少量 OS 线程就能支撑海量并发 I/O

  1. 回归同步写法,简化编程模型
  • • 你可以继续写“顺序代码”:val = call(); process(val);

  • • 底层自动在 I/O 处挂起/恢复,不再需要回调地狱或响应式流。

  1. 与语言/平台生态整合
  • • Java:虚拟线程仍然是Thread,兼容synchronizedExecutorService等,尽量少改既有代码。
传统Java线程和虚拟线程的对比
特性平台线程(传统 Java 线程)虚拟线程(Virtual Thread)
映射关系1:1(线程 = OS 线程)M:N(虚拟线程:平台线程)
栈大小默认 ~1MB初始几百字节~几 KB,按需增长
创建/上下文切换毫秒级,内核态微秒/纳秒级,用户态
可轻松创建数量级数千百万级
阻塞行为阻塞 OS 线程仅挂起自己,载体线程可复用

20. jdk21的虚拟线程适合什么场景,不适合什么场景?

**虚拟线程最适合:大量“等 I/O、短任务、高并发”的场景,尤其是用“一个请求一个线程”写的服务端程序。**对 CPU 密集型任务几乎没有帮助,甚至更差。

20.1. 虚拟线程表现最好的典型场景

20.1.1 高并发 Web / API 服务(thread-per-request)

官方明确说,虚拟线程的目标就是让简单的 “thread-per-request 服务器应用能接近硬件极限扩展

特点:

  • • 每个请求一个虚拟线程,代码结构几乎不变

  • • 大量请求在等 I/O:等 DB、等缓存、等下游服务

  • • 传统线程池几千线程就到瓶颈,虚拟线程可以轻松上十万、百万级

适合:

  • • REST API / BFF 网关

  • • 电商/支付等高并发短请求服务

  • • 任何用 “一个请求一个线程” 写的服务端程序

20.1.2 微服务之间大量调用(HTTP / RPC / Feign / Dubbo)

一个请求经常要串行或并行调用多个下游服务,大部分时间在等网络返回。

传统做法:

  • • 用异步 Callback / Reactor / WebFlux,避免线程池被打满

  • • 代码复杂,调试困难

用虚拟线程:

  • • 继续写同步阻塞调用:http.get(),rpc.call()

  • • 阻塞时虚拟线程自动让出载体线程,少量载体线程就能支撑海量并发请

适合:

  • • 服务间 A 调 B、B 调 C 的同步链路

  • • 重构现有异步代码为同步风格,同时保持吞吐

20.1.3 高并发 JDBC / NoSQL 访问

典型情况:每个请求都要查几次数据库或 Redis,线程大部分时间在等 I/O。

优势:

  • • 虚拟线程阻塞在connection.executeQuery()时,载体线程可以服务其他请求

  • • 不再需要“大连接池 + 大线程池”的复杂配置,线程数几乎不再成为瓶颈

适合:

  • • 高并发短查询的业务系统

  • • 批量查询 / 批量更新(每次 I/O 很短,但量很大)

20.1.4 消息队列消费者(Kafka / RocketMQ / RabbitMQ)

消费逻辑通常是:收消息 → 调用远程服务 / 写库 → 确认。大部分时间在等 I/O。

传统问题:

  • • 线程池大小配置难:太少吞吐低,太多 OOM

  • • 异步消费者回调模型复杂

虚拟线程方式:

  • • 每条消息一个虚拟线程,send()/call()阻塞也不怕

  • • 可以非常方便地提高并发消费能力,不用调线程池

适合:

  • • I/O 密集型消费者(调下游服务、写 DB)

  • • 需要线性提高消费并发度的场景

20.1.5 大量文件 / 网络 I/O 的批处理

例如:成千上万小文件读取、外部 API 批量调用、日志/埋点处理等。

特点:

  • • 每个任务 I/O 等待时间长,CPU 闲置

  • • 虚拟线程可以在等 I/O 时把载体线程让给别人,整体吞吐提升明显

适合:

  • • 批量导入/导出、数据同步

  • • 大量短 I/O 任务(每任务几十毫秒~几百毫秒)

20.2. 虚拟线程“不太合适”的场景

20.2.1 CPU 密集型任务

官方和大量实践都强调:虚拟线程不是更快的线程,而是更“可扩展”的线程

  • • 如果任务主要是 CPU 计算(加密、压缩、图像/视频处理、复杂算法),虚拟线程几乎没有优势,甚至因调度开销略慢

  • • 虚拟线程默认的载体线程数量 ≈ CPU 核心数,长计算会占死载体线程,其他虚拟线程排不到

建议:

  • • CPU 密集任务仍用 **平台线程池(ThreadPoolExecutor)或并行流(parallel stream。
20.2.2 需要精细控制线程数 / 亲和性的场景

例如:

  • • 需要固定线程数绑定 CPU 缓存、NUMA 优化

  • • 需要严格限制并发访问某个资源(如外部服务限流)

虚拟线程非常“轻”,可以轻易创建百万个,反而容易把下游打挂。此时更适合:

  • • 显式使用固定大小的平台线程池 + 信号量/限流

  • • 虚拟线程只在I/O 等待阶段使用,CPU 部分仍用平台线程

21. jdk21中,虚拟线程什么情况下阻塞会被钉住(pinned)?

  • 会被“钉住”(pinned):虚拟线程在synchronized块/方法里阻塞,或者调本地方法(native / foreign function)时阻塞——载体线程不会释放,其他虚拟线程用不了这个载体线程。

  • 不会被钉住:**不是在synchronized/ native 里的阻塞,一般都可以卸载。普通 JDK 的阻塞 I/O(socket/文件/Channel)、Thread.sleepObject.waitLockSupport.parkBlockingQueue.take等——虚拟线程会正常从载体线程卸载,载体线程可以被别的虚拟线程复用。

什么情况下阻塞会钉住?

synchronized块/方法里的阻塞

典型代码(JDK 21–23)

synchronized(lock){// 这里做阻塞 I/O 或sleepsocket.read();// 或 Thread.sleep(1000)}
  • • 虚拟线程进入synchronized后,监视器锁的 owner 记录的是载体线程(平台线程),而不是虚拟线程。

  • • 如果虚拟线程在这里卸载,载体线程换人,锁的 owner 就对不上了,会破坏互斥语义,所以 JVM不允许卸载,直接把虚拟线程“钉”在当前载体线程上。

  • • 结果:socket.read()/Thread.sleep()虽然是“可卸载”操作,但因为外面包着synchronized,虚拟线程无法卸载,载体线程一起阻塞,直到离开synchronized块。

关键点:

  • • 不是Thread.sleep/ I/O 本身钉住,而是synchronized里面做这些操作才钉住。

  • • 只要还在synchronized块内,哪怕只是Thread.sleep(1),也会钉住载体线程。

native / foreign function 里的阻塞

例如:

  • • 调用 JNI 函数,内部做了阻塞读网络 / 文件;

  • • 使用 Panama / foreign function API 调外部 C 函数,该函数里阻塞。

JEP 444 把这些归为第二类钉住点:

  • • native / foreign 函数对 JVM 来说是“黑盒”,JVM 无法安全地保存/恢复 Java 栈帧,因此不能卸载,只能阻塞载体线程。

  • • 这类代码如果频繁长时间阻塞,也会严重拖垮并发。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 12:09:56

mysqldump-vs-xtrabackup

mysqldump 和 xtrabackup 的区别、场景与常见问题 mysqldump 是逻辑备份工具,导出的是 SQL 语句。xtrabackup 是物理备份工具,备份的是 MySQL 底层数据文件。 简单理解: mysqldump 把数据库导出成 SQL 文件 xtrabackup 给 MySQL 数据文件做…

作者头像 李华
网站建设 2026/6/15 12:06:50

AI写专著全攻略:从构思到定稿,AI专著生成工具3天搞定20万字!

学术专著写作困境与AI工具解决方案 撰写学术专著的过程,往往在“内容深度”和“覆盖广度”之间面临诸多挑战,这也是不少学者所遭遇的瓶颈。在深度方面,专著的核心内容需具备充分的学术价值,不仅要清楚地回答“是什么”&#xff0…

作者头像 李华
网站建设 2026/6/15 12:05:58

Prompt工程、DINOv2视觉嵌入与OLMo 2模型选型实战指南

1. 这不是“调参”,而是构建人机协作的底层能力——从LAI #84标题看大模型时代的真实工作流你点开这个标题时,大概率不是为了查论文摘要,而是想确认:这期内容里有没有我能立刻用上的东西?有没有我正在卡壳的问题的答案…

作者头像 李华