在 Java 中,线程安全是一个核心概念,指的是当多个线程并发访问某个类、对象或方法时,这个类、对象或方法始终能表现出正确的行为,而无需额外的同步或协调机制。


核心问题:共享数据的并发访问

  • 线程不安全的根源在于多个线程同时读写共享的可变状态(数据)
  • 如果数据是只读的(不可变),或者每个线程操作的都是自己私有的数据副本,那么自然就是线程安全的。
  • 问题在于:当多个线程尝试同时修改同一个变量、同一个集合、同一个文件等共享资源时,如果访问操作不是原子的,或者线程间的修改对其他线程不可见,就会导致数据不一致、逻辑错误、程序崩溃等不可预测的后果。

导致线程不安全的三大根源

  • 原子性缺失: 一个操作(可能由多条字节码或机器指令组成)需要作为一个不可分割的整体执行。如果执行到一半被其他线程打断,就可能导致中间状态被其他线程观察到或修改,从而破坏一致性。
    • 经典例子:i++ 它实际上包含读取i的值、将值加1、将结果写回i三个步骤。两个线程同时执行i++可能导致只增加一次。
  • 可见性问题: 一个线程对共享变量的修改,不能立即被其他线程看到。这主要是由现代计算机架构(多级缓存)和 Java 内存模型(JMM)允许线程将变量保存在本地寄存器或缓存中引起的。
    • 经典例子: 线程 A 修改了变量 flag = true,但线程 B 可能在一段时间内(甚至永远)看不到这个变化,仍然读取到旧的 false 值。
  • 有序性问题: 程序代码的执行顺序不一定就是实际指令的执行顺序。编译器和处理器为了优化性能,可能会进行指令重排序。在单线程环境下,这通常不会影响结果;但在多线程环境下,如果重排序破坏了线程间的依赖关系,就可能出现意想不到的行为。
    • 经典例子: 双重检查锁定(DCL)的单例模式实现(在旧版本 Java 中)可能因为重排序导致返回一个未完全初始化的对象。

如何实现线程安全

  • 无状态: 最简单的方法。类不包含任何字段(状态),或者只包含常量字段。所有操作仅依赖于参数和局部变量,这些都在线程栈上,是线程私有的。(线程安全)
  • 不可变对象: 对象的状态在构造完成后就不能再被修改。所有字段都是 final 的,并且如果字段是引用类型,它指向的对象也应该是不可变的(或安全发布)。线程只能读取,不存在修改冲突。(线程安全) String, Integer 等是典型例子。
  • 线程封闭: 将共享数据的访问限制在同一个线程内。
  • 栈封闭: 局部变量是线程私有的。确保可变对象不逸出方法范围。
  • ThreadLocal 为每个线程创建变量的独立副本。常用于存储用户会话信息、数据库连接等。
  • 同步(Synchronization): 使用锁机制强制互斥访问共享数据。
    • synchronized 关键字:
      • 修饰实例方法:锁是当前对象实例 (this)。
      • 修饰静态方法:锁是当前类的 Class 对象。
      • 同步代码块 (synchronized(obj) { ... }):锁是指定的对象 obj
      • 作用:保证原子性(代码块内操作不可分割)和可见性(释放锁前将修改刷回主内存,获取锁后清空本地缓存)。它也会限制临界区内的重排序,间接保证有序性。
    • java.util.concurrent.locks.Lock 接口 (如 ReentrantLock):
      • 提供比 synchronized 更灵活的锁操作(可中断、尝试获取锁、公平锁等)。
      • 同样保证原子性和可见性(通过内存屏障)。
  • volatile 变量:
    • 解决可见性问题:对 volatile 变量的写操作会立即刷新到主内存,读操作会直接从主内存读取。
    • 解决部分有序性问题:禁止指令重排序优化(内存屏障)。写 volatile 之前的操作不会被重排序到写之后;读 volatile 之后的操作不会被重排序到读之前。
    • 不保证原子性! volatile 不能替代 synchronized 或锁来处理复合操作(如 count++)。
    • 典型用途:状态标志位 (boolean flag),双重检查锁定(需要配合 volatile 解决重排序问题)。
  • 原子变量 (java.util.concurrent.atomic 包):
    • 提供如 AtomicInteger, AtomicLong, AtomicReference 等类。
    • 利用 CAS (Compare-And-Swap) 硬件指令实现单个变量的原子操作(如 getAndIncrement(), compareAndSet())。
    • 高效地解决原子性问题(针对单个变量),通常比锁性能更好。也提供可见性保证。
  • 并发容器 (java.util.concurrent 包):
    • 提供线程安全的集合类,如 ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, BlockingQueue 等。
    • 内部通常结合了 CAS、锁、分段锁等机制实现高效的并发访问。优先使用这些容器代替手动同步 Collections.synchronizedXXX 包装的集合。
  • 避免共享: 设计上尽量降低共享数据的必要性(如使用线程局部变量、任务分发、Actor 模型等)。

线程安全的级别/分类

  • 不可变: 最高级别的线程安全。
  • 无条件的线程安全: 类的实例是可变的,但内部有足够的同步机制(如 synchronized 方法),客户端调用任何方法都不需要额外的同步(如 ConcurrentHashMap, AtomicInteger)。
  • 有条件的线程安全: 类本身是线程安全的,但某些复合操作(需要按特定顺序调用多个方法)需要客户端进行额外的同步(如 Collections.synchronizedMap 返回的 Map,其 getput 单个方法是安全的,但迭代或 putIfAbsent 这样的复合操作需要客户端加锁)。
  • 非线程安全: 类不提供任何同步机制(如 ArrayList, HashMap)。客户端必须在并发访问时自行协调同步。
  • 线程对立: 无论客户端是否同步,都无法安全地在多线程中使用(极其罕见,通常是设计错误)。

总结与关键点:

  1. 核心: 线程安全的核心在于管理对共享、可变状态的并发访问
  2. 根源: 原子性缺失、可见性问题、有序性问题是导致线程不安全的三大根源。
  3. 策略:
    • 避免共享: 无状态、不可变、线程封闭。
    • 安全共享: 使用同步机制 (synchronized, Lock) 保证原子性和可见性。
    • 高效共享: 使用原子变量、并发容器(内部通常基于 CAS)。
    • 可见性与有序性: 使用 volatile 或同步机制。
  4. 选择: 没有绝对最好的方案。选择哪种策略取决于具体的应用场景、性能要求、复杂性等因素。优先考虑使用 JDK 提供的并发工具 (java.util.concurrent)。
  5. 设计原则: 尽量设计无状态或不可变对象;最小化共享数据的范围;优先使用线程安全的组件;明确文档化类的线程安全属性。