Java 中如何实现线程安全?

考查重点:共享可变状态与完整不变量。先找共享可变状态,能不共享或不可变优先,否则用锁/原子/并发容器保护完整业务不变量

本题目录

参考答案

核心回答

Java 线程安全的目标是:多个线程按照约定并发使用一份状态时,仍能保持业务结果和状态约束正确。实现时先找出共享的可变状态及必须一起成立的条件,再选择保护方式:能不共享就做线程封闭,能不变就用不可变对象;必须共享修改时,用锁保护完整业务操作,或使用原子类、并发容器提供的适当原子操作,并保证正确的发布与可见性。

volatile、synchronized、原子类和并发集合解决的范围不同,不能只看某个字段或方法是否“线程安全”,就认为组合起来的整个流程也安全。

先判断需要保护什么

一个计数器的 count++ 包含读取、计算、写回。两个线程可能都读到 100,再各自写回 101,丢失一次更新;把 count 改成 volatile 也不会把这组操作合成原子操作。

更复杂的例子是“检查库存足够,再扣库存”。即使读取和扣减各自是原子方法,两者之间仍可能被其他线程插入操作。真正需要保护的是“检查与扣减共同成立,且库存不为负”的业务约束。

因此要同时考虑原子性、可见性和相关的执行顺序:写入能否被正确读取,是否可能读到不一致的字段组合,以及整个读改写过程是否允许交错。

常见实现方式及适用范围

减少共享和修改

方法内创建、不会逃逸给其他线程的可变对象,可以由当前线程独占使用。局部变量本身在线程内,不代表它引用的对象也一定私有;多个线程仍可能通过不同引用操作同一对象。

不可变对象适合共享读取,但要真正封住可变部分:final 引用只限制重新赋值,不会自动让它指向的数组、集合及元素不可变。构造期间也不应把尚未初始化完成的对象暴露出去。需要更新配置快照时,可以构造一个完整的新快照,再通过具有正确发布语义的引用交给读线程。

用锁保护完整的状态约束

synchronized 或 Lock 可以保护涉及多个字段、多个步骤的临界区。所有相关访问必须遵守同一套锁规则:写入加锁,读取却绕过锁,仍可能存在可见性或快照不一致问题;两个线程各锁一个不同对象,也没有互斥效果。

synchronized 实例方法锁当前实例,静态同步方法锁对应的 Class 对象。显式锁适合需要定时获取、可中断等待等能力的场景,取得锁后要在适当的 finally 中释放。选择哪一种取决于控制需求,不能笼统认为某一种始终更安全或更快。

锁内完成必要的判断和更新,注意多锁的获取顺序。锁释放能让其他线程继续访问,却不会自动回滚异常发生前已经修改的字段,所以异常路径也要维护业务约束。

用原子类完成相应的单项原子操作

独立计数可用 AtomicInteger 等原子类的增量操作;条件替换可用 CAS。它们适合能表达为相应原子更新的状态,不应把分别更新两个原子变量当作一个事务。

若多个值必须一起变更,可以把它们组成不可变状态,通过一个原子引用替换,或统一加锁。CAS 失败后的重试要重新读取和判断;也要考虑 ABA、竞争成本以及重试时重复执行外部副作用的问题。

使用并发容器,但检查组合操作

共享 Map 可以考虑 ConcurrentHashMap;需要“存在则更新”或“缺失则计算”时,使用它提供的条件更新、compute 或 computeIfAbsent 等对应能力,而不是自己拆成先 containsKey 再 put。

这些能力有具体边界:Map 安全不等于存入的可变对象也安全;某个键的原子更新不等于跨两个键的转账原子;遍历结果也未必是整个 Map 的同一时刻快照。计算函数应短小,不应在其中再随意修改同一 Map 或执行难以恢复的慢业务。

volatile 适合可见性和发布,不承担所有同步

对同一个 volatile 变量的写与后续读之间建立相应的 happens-before 关系,可用于简单状态标志或完整快照引用的发布。它不会给任意代码块加互斥,也不会让引用对象内部之后发生的任意修改自动安全。

锁的解锁与之后对同一监视器的加锁、线程启动和成功等待线程结束等,也能建立相应关系。正确性应建立在这些语义上,而不是靠 sleep、多打几行日志或假设某个 CPU 执行顺序。

如何验证设计是否成立

先明确可检查的结果,例如总增量不丢失、库存不为负、两个相关字段始终属于同一版本。然后针对同一共享对象制造并发调用,覆盖检查与更新交错、异常退出、锁获取失败和容器内可变值更新等路径。

测试可以暴露错误,但一次甚至多次运行没错,都不能证明所有调度下正确。还需要从共享范围、原子边界与 happens-before 关系说明设计为何成立。synchronized 等 JVM 内同步也只覆盖相应进程内的共享状态;多个服务实例同时修改数据库订单时,还需要数据库事务、约束或相应的跨实例协调。

常见追问

只读操作是否天然安全?

如果对象已正确构造并发布,且之后没有并发修改,共享读取通常不需要额外互斥。但若其他线程还在改,读取也必须遵守可见性和一致快照的协议。

ThreadLocal 能否替代锁?

它用于把数据关联到线程,适合隔离各线程自己的状态,不会保护所有线程共同修改的同一业务对象。线程池复用线程时,还要管理数据的初始化和清理,避免状态串用。

选型时先考虑性能还是正确性?

先确定最小且完整的业务约束和同步范围,再根据真实争用、读写比例与等待成本优化。锁太大影响并行,锁太碎或原子步骤拆得太细则可能直接破坏正确性。

参考资料

  • 国内:JavaGuide:Java 内存模型:参考共享变量、happens-before、原子性与可见性,解释正文中正确更新边界与读写同步关系为何都不可缺。
  • 国内:JavaGuide:Java 并发容器总结:借鉴并发 Map、写时复制与阻塞队列的分工,区分正文中容器单方法的保证与业务复合操作的原子范围。
  • 国内:廖雪峰:线程同步:采用计数器交错与共享锁的例子,解释正文中的丢失更新及同一锁协议为何要覆盖完整共享状态。
  • 国外:Java SE 25 语言规范:Threads and Locks:同步、happens-before 和 final 字段语义,是判断锁、volatile 与对象初始化发布边界的规范依据;单次赋值原子不代表可以忽略可见性。
  • 国外:Java SE 25:ConcurrentHashMap:依据条件更新、计算方法与 Map 整体观察的契约,解释正文中并发容器的方法安全为何不能替代跨键业务事务。

相关面经

8 篇

哔哩哔哩

Java 后端开发工程师
一面
查看原面经 →
Java如何实现线程安全。

科大讯飞

Java 开发
一面
查看原面经 →
线程安全的方法有哪些?

蚂蚁集团

面经合集
查看原面经 →
什么是线程安全,Java 如何实现线程安全

蚂蚁集团

面经合集
查看原面经 →
保证线程安全的方法有哪些?

蚂蚁集团

面经合集
查看原面经 →
多线程场景下,如何保证线程安全
展开剩余 3 篇面经

广电数科

Java 开发工程师
实习
查看原面经 →
java多并发应该如何解决

美团

软件开发工程师
秋招一面
查看原面经 →
Java里面是怎么实现线程安全的?

BIGO

客户端开发工程师
秋招二面
查看原面经 →
多线程如何保证进程安全?
追问
Java中哪些关键字可以做到?

收录题集

1 个来源,共1 份题集

相关题目

4 题