真实面经题目 · 原创解析

什么是线程安全?

线程安全指一段代码、对象或组件在多个线程同时访问时,仍然能够保持预期的正确性,不因为执行时序交错而产生脏数据、丢失更新、状态破坏或偶发异常。判断线程安全不能只看单次调用是否正确,而要看共享可变状态在并发读写下是否满足原子性、可见性和有序性,并通过不可变、线程封闭、同步、原子类或并发容器等手段建立可靠的并发语义。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

线程安全是指在多线程环境下,多个线程同时访问某段代码、某个对象或某份数据时,不需要调用方额外做错误的时序假设,程序仍然能得到符合设计预期的结果。它的核心不是“有没有加锁”,而是“并发访问下状态是否始终正确”。最常见的问题来自共享可变状态:多个线程读写同一个变量、集合、缓存或对象字段,如果复合操作被打断,就会出现竞态条件,例如先读再改再写的累加操作可能丢失更新。判断线程安全通常从三个角度展开:原子性要求关键操作不可被中间状态观察或打断;可见性要求一个线程修改的数据能被其他线程及时看到;有序性要求编译器、CPU 或运行时的重排序不能破坏程序语义。解决方式包括使用锁、读写锁、synchronized、volatile、原子类、并发集合,也可以通过不可变对象、局部变量、ThreadLocal、消息传递、Actor 模型等方式减少共享。面试中可以强调:线程安全是一种正确性保证,必须结合共享资源、访问方式、不变式和并发边界来分析,而不是简单等同于“加了锁”或“没有并发异常”。

考点 定义边界
主线 对象状态
易错点 把线程安全简单等同于加锁,忽略不可变、线程封闭和并发设…

深入解析

01

定义边界

线程安全描述的是并发访问下的正确性,关注结果是否符合设计约束,而不是代码是否能偶尔运行成功。回答时要把它放到多线程同时访问的上下文里,而不是只讨论单线程下某个方法能否返回正确值,还要说明不同线程交错执行时对象不变式是否仍然成立。

02

对象状态

一个对象是否线程安全,取决于它的内部状态是否会被多个线程共享,以及这些状态在读写时是否被保护。没有共享状态的纯函数、不可变对象和方法内局部变量通常风险较低;共享缓存、集合、计数器、连接状态则需要重点分析。

03

共享可变状态

线程安全问题主要来自共享可变状态。只读数据或不可变数据即使被多个线程访问也不容易出错,而多个线程同时修改同一份状态时,程序结果就可能依赖不可控的执行时序,产生丢失更新、重复初始化或状态覆盖。

04

竞态条件

当程序结果依赖线程执行先后顺序时,就可能存在竞态条件。典型例子是 check-then-act、read-modify-write、延迟初始化和遍历时修改集合:单步操作看似简单,但组合起来如果没有同步边界,就会暴露中间状态。

05

原子性要求

原子性要求关键操作不能被其他线程观察到中间状态,也不能在执行过程中被破坏。i++、余额扣减、库存扣减、先查再插等操作都不是天然原子,必须通过锁、CAS、数据库约束或事务性机制保护完整不变式。

06

可见性要求

一个线程写入的值不一定会立即被另一个线程看到,原因可能是 CPU 缓存、编译器优化或运行时内存模型。volatile、锁释放与获取、并发工具类的 happens-before 语义都可以建立可见性关系,但可见性不等于复合操作原子。

07

有序性要求

编译器和处理器可能进行指令重排序,单线程语义不变并不代表多线程观察结果也不变。对象发布、双重检查锁、初始化状态标记等场景尤其要小心,因为其他线程可能看到未完全初始化或顺序异常的状态。

08

同步机制

锁、synchronized、ReentrantLock、读写锁、信号量等机制能保护临界区,但关键是锁住完整不变式,而不是随意包一段代码。锁粒度过大影响并发,锁粒度过小无法保护一致性,还可能引入死锁、锁顺序和超时问题。

09

无锁工具

AtomicInteger、AtomicReference、CAS、并发队列等工具可以减少阻塞,适合单变量或特定结构的并发更新。但它们不是万能方案,仍要理解 ABA、自旋成本、复合不变式和失败重试,否则容易把复杂状态拆碎后失去整体一致性。

10

设计优先

更稳的思路是先减少共享:不可变对象、线程封闭、局部变量、ThreadLocal、消息传递和清晰所有权都能降低并发复杂度。只有必须共享的状态才引入同步机制,并通过测试、压测和代码审查验证并发语义。

易错点

  • 把线程安全简单等同于加锁,忽略不可变、线程封闭和并发设计。
  • 认为单个变量读写没报错就一定线程安全。
  • 忽略 i++、check-then-act、lazy init 这类复合操作的竞态风险。
  • 误以为 volatile 可以保证所有操作的原子性。
  • 使用线程安全集合后,又在外部执行非原子的组合逻辑。
  • 锁粒度过大导致性能下降,锁粒度过小又无法保护完整不变式。
  • 在构造函数或初始化阶段让未完全构造的对象逃逸。
  • 只在本机压测通过就认定没有并发问题,忽略时序偶发性。

面试官追问

volatile 能不能保证线程安全?为什么它通常不能替代锁?

volatile 主要保证可见性和一定的有序性,不能让 i++、检查后更新、多字段更新这类复合操作变成原子操作。只有单变量状态发布、开关标记等简单场景适合直接使用 volatile。

synchronized 和 ReentrantLock 在语义和使用场景上有什么区别?

两者都能提供互斥和可见性。synchronized 语法简单、自动释放锁;ReentrantLock 支持可中断、限时获取、公平锁和多个条件队列,适合控制需求更复杂的并发场景。

AtomicInteger 的 incrementAndGet 为什么是线程安全的?

它基于 CAS 或类似原子指令完成读取、加一、写回的整体更新,失败时会重试,避免多个线程同时读到旧值后覆盖彼此的更新。但它只保护这个原子变量本身。

线程安全的集合是否意味着所有组合操作都线程安全?

不一定。单次 put、get 可能是线程安全的,但先 contains 再 put、遍历后批量删除、多集合联动更新仍然是组合逻辑,需要额外同步或使用专门的原子 API。

什么是安全发布?对象构造后如何保证其他线程看到完整状态?

安全发布是让其他线程看到对象引用时,也能看到构造期间写入的完整字段状态。常见方式包括 final 字段语义、volatile 引用、锁保护、静态初始化或通过线程安全容器发布。

如何排查线上偶发的并发问题?

先定位共享状态和并发入口,再结合日志、线程栈、锁等待、指标波动和复现压测缩小范围。对偶发问题要关注时序、重试、缓存可见性、未同步集合和对象发布路径。