真实面经题目 · 原创解析

Java 实现线程安全的方式?

线程安全的核心不是某个关键字,而是多个线程访问共享可变状态时,程序仍能保持正确性。回答时应先界定共享、可变、并发访问这三个条件,再按避免共享、限制共享、控制访问、使用并发工具和改进设计几个层次展开。

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

60 秒回答模板

Java 实现线程安全可以从本质和手段两部分回答。本质上,线程安全是对共享可变状态的正确访问:如果数据不共享,或者共享但不可变,通常天然安全;如果多个线程会同时读写同一份可变数据,就必须保证可见性、原子性和有序性。常见方式包括使用无状态对象、不可变对象、局部变量和 ThreadLocal 做线程封闭;用 synchronized、ReentrantLock、读写锁等互斥机制保护临界区;用 volatile 保证可见性和禁止部分重排序,但不能保证 i++ 这类复合操作的原子性;用 AtomicInteger、AtomicReference 等基于 CAS 的原子类处理简单并发更新;用 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue 等并发容器替代手写同步。更高层次上,应尽量减少共享状态,把状态收敛到更小范围,使用消息传递、不可变快照、任务队列或数据库事务来降低并发复杂度。

考点 先判断是否存在共享可变状态
主线 不可变和线程封闭
易错点 把线程安全简单等同于加 synchronized,忽略…

深入解析

01

先判断是否存在共享可变状态

线程安全问题通常同时满足三个条件:有共享数据、数据会变化、存在多个线程并发访问。若对象是无状态的,方法只依赖入参和局部变量,局部变量又存在线程私有栈中,一般不会产生线程安全问题。因此回答不要一上来只说加锁,而要先说明能不共享就不共享,能不可变就不可变,这是成本最低也最稳定的方案。

02

不可变和线程封闭

不可变对象创建后状态不再改变,只要构造过程没有逸出,就可以被多个线程安全共享,例如只读配置、值对象、不可变集合快照。线程封闭则是把状态限制在单个线程内,例如方法局部变量、单线程事件循环、ThreadLocal 保存每个线程独立副本。它们的共同点是减少竞争,不需要频繁阻塞,也能降低锁使用错误带来的风险。

03

互斥锁保护临界区

当多个线程必须修改同一份共享状态时,需要用 synchronized、ReentrantLock 或读写锁保护临界区。synchronized 使用简单,进入和退出同步块自动释放锁;ReentrantLock 支持可中断、限时等待、公平锁等更灵活能力;读写锁适合读多写少场景,让多个读线程并发进入,写线程独占。锁的关键不是包住代码越多越好,而是准确包住共享状态的不变式。

04

volatile 不替代锁

volatile 能保证一个线程写入后,其他线程能及时看到最新值,并能约束相关指令重排序,所以常用于状态标记、开关变量、单次发布等场景。但它不保证复合操作的原子性,例如 count++ 包含读取、计算、写回多个步骤,多个线程仍可能丢失更新。回答时要明确 volatile 适合一写多读或独立赋值,不适合维护复杂一致性。

05

CAS 和原子类

AtomicInteger、AtomicLong、AtomicReference 等原子类通常基于 CAS 思路:比较当前值是否仍是预期值,是则更新,否则重试。它避免了重量级阻塞,适合计数器、状态机引用切换、简单累加等场景。但 CAS 也有自旋开销、ABA 问题和只能自然处理单变量更新的限制;涉及多个变量共同满足约束时,仍需要锁或更高层事务机制。

06

并发容器降低复杂度

Java 并发包提供了大量经过验证的容器和同步工具,例如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue、Semaphore、CountDownLatch、CyclicBarrier 等。它们内部已经处理了并发访问、内存可见性和部分性能优化,通常优先于自己用普通 HashMap 加零散锁。选择时要结合读写比例、是否需要阻塞、是否需要快照语义和一致性要求。

07

设计层面减少共享

真正成熟的线程安全方案往往来自设计,而不只是某个 API。可以通过拆分状态所有权、使用不可变 DTO、队列串行化写操作、Actor 或事件驱动模型、数据库唯一约束和事务等方式,把并发冲突限制在少数边界。这样代码更容易推理,也更容易测试。强调设计层面的收敛,能体现对线程安全本质的理解。

易错点

  • 把线程安全简单等同于加 synchronized,忽略了无共享、不可变和线程封闭这些更基础的解决方式。
  • 认为 volatile 可以替代锁,直接用它处理自增、余额扣减、检查后更新等复合操作。
  • 使用普通 HashMap、ArrayList 在多线程下读写,然后只在个别位置加锁,导致保护范围不完整。
  • ThreadLocal 用在线程池场景后不清理,造成后续任务读到旧上下文或长期持有无用对象。
  • 锁住了不同对象或锁粒度前后不一致,导致看似加锁但共享状态仍然被并发修改。
  • 只关注 API 名称,无法说明原子性、可见性、有序性和共享可变状态之间的关系。

面试官追问

synchronized 和 ReentrantLock 有什么区别?

synchronized 是 JVM 层面的内置锁,语法简单,异常时会自动释放锁,适合大多数普通互斥场景。ReentrantLock 是显式锁,必须在 finally 中释放,但支持可中断等待、尝试加锁、超时等待、公平锁和多个 Condition,适合需要更精细控制的并发逻辑。

为什么 volatile 不能保证 count++ 的线程安全?

count++ 不是单条不可分割操作,而是先读取当前值,再加一,最后写回。volatile 只能保证读写这个变量时的可见性,不能阻止两个线程同时读到同一个旧值并分别写回相同结果,因此仍可能发生更新丢失。

什么时候使用 ThreadLocal?

ThreadLocal 适合把本来不该在线程间共享的数据绑定到当前线程,例如请求上下文、用户身份、链路追踪 ID、日期格式化对象等。它不是用来解决共享变量竞争的锁机制,而是通过线程隔离避免共享。在线程池中使用后要 remove,防止数据串扰和内存滞留。

CAS 和 synchronized 应该怎么选择?

如果是单个变量的简单更新,例如计数、状态位切换、引用替换,CAS 和原子类通常更轻量。如果涉及多个变量、一组操作必须整体满足不变式,或者竞争很激烈导致大量自旋失败,使用 synchronized 或 ReentrantLock 往往更清晰可靠。

ConcurrentHashMap 一定能保证业务逻辑线程安全吗?

ConcurrentHashMap 能保证容器内部结构在并发访问下不被破坏,并提供一些原子方法,例如 putIfAbsent、compute、merge。但如果业务逻辑是先 get 再判断再 put,多个步骤组合起来仍可能有竞态,应使用容器提供的原子方法或额外同步。