3、线程安全基础:什么是线程安全、原子性、可见性、有序性、Java内存模型(JMM)概述

好,咱们今天聊聊线程安全的基础。说实话,这章内容看着基础,但很多线上事故的根子就在这。我见过不少工作三五年的同事,聊起并发框架头头是道,但一问到JMM就含糊了。嗯,咱们先把地基打牢。

3.1 到底什么是线程安全?

线程安全这个概念,说白了就是:多个线程同时访问一个共享数据时,程序的行为仍然是正确的。你想想看,如果一段代码在单线程下跑得好好的,一到多线程就出幺蛾子,那它就是不安全的。

我个人习惯用一个简单的标准来判断:不管线程怎么调度、怎么交替执行,都不需要调用方做额外的同步措施,这个类就是线程安全的

核心定义:当多个线程访问某个类时,这个类始终表现出正确的行为,那么它就是线程安全的。

我在项目中遇到过最典型的反例——一个计数器类,多个线程同时调用count++。结果呢?本该是1000的最终值,经常变成997、998。这就是典型的线程不安全。

3.2 原子性——要么全做,要么全不做

原子性,就是操作不可分割。一个操作要么全部执行完,要么一步都没执行。不会出现执行到一半被其他线程插一脚的情况。

举个例子,count++看起来是一条语句,但在CPU层面其实是三步:

  1. 从内存读取count的值
  2. 把值加1
  3. 把新值写回内存

这三步之间,随时可能被其他线程打断。这就是为什么最终结果不对。

避坑指南:我曾经在写一个库存扣减服务时,用了普通的int变量做扣减。压测时发现库存变成了负数。后来改成AtomicInteger,问题就解决了。记住:i++不是原子操作,AtomicIntegerincrementAndGet()才是。

Java中保证原子性的方式主要有:

  • synchronized:重量级,但简单可靠
  • Lock接口:更灵活,支持尝试获取、超时等
  • Atomic包下的类:基于CAS,无锁并发,性能好

3.3 可见性——改了别人看得见吗?

可见性问题,说白了就是一个线程修改了共享变量,其他线程能不能立刻看到。

为什么会这样?因为每个线程都有自己的工作内存(CPU缓存)。线程A改了变量,可能还留在自己的缓存里,没写回主内存。线程B读的时候,读到的还是旧值。

经典案例:我调过一个线上问题,一个boolean类型的running标志位,主线程把它设成false,但子线程就是停不下来。原因就是子线程一直读自己缓存里的旧值,根本看不到主线程的修改。加上volatile关键字后,问题立刻解决。

保证可见性的方式:

  • volatile:保证每次读取都从主内存拿,每次写入都立即刷回主内存
  • synchronized:释放锁时会把修改刷回主内存
  • final:构造器完成后,其他线程看到的一定是最新值

3.4 有序性——代码真的按顺序执行吗?

你写代码时,觉得语句是按顺序执行的。但CPU和编译器为了优化性能,可能会指令重排序。只要重排序不影响单线程的结果,它们就敢干。

但在多线程环境下,重排序可能带来灾难。

// 线程1
config = loadConfig();   // 语句1
initialized = true;      // 语句2

// 线程2
if (initialized) {
    useConfig(config);   // 可能拿到不完整的config
}

你想想看,如果语句2被重排序到了语句1前面,线程2看到initialized=true时,config可能还没加载完。这就是有序性问题。

关键点:Java内存模型允许编译器和处理器进行指令重排序,但会通过happens-before规则来限制哪些情况下不能重排序。

3.5 Java内存模型(JMM)概述

JMM,说白了就是一套规范。它规定了:

  • 线程怎么读写共享变量
  • 什么时候一个线程的写对另一个线程可见
  • 哪些重排序是允许的,哪些不允许

JMM把内存分为主内存工作内存

内存区域 说明
主内存 所有线程共享,存储所有变量
工作内存 每个线程私有,存储主内存变量的副本

线程操作变量的流程:

  1. 从主内存拷贝变量到工作内存
  2. 在工作内存中执行操作
  3. 把结果写回主内存

嗯,这里要注意:JMM是一个抽象模型,它不关心底层是CPU缓存还是寄存器。它只定义了一套规则,让Java程序员不用操心不同硬件平台的差异。

3.6 happens-before规则——JMM的核心

JMM通过happens-before规则来保证有序性和可见性。如果两个操作满足happens-before关系,那么前一个操作的结果对后一个操作是可见的,且前一个操作的执行顺序在后一个操作之前。

常见的happens-before规则:

  • 程序次序规则:同一个线程中,写在前面的代码happens-before后面的代码
  • volatile变量规则:对一个volatile变量的写,happens-before后续对这个变量的读
  • 锁规则:解锁操作happens-before后续的加锁操作
  • 传递性:如果A happens-before B,B happens-before C,那么A happens-before C

我的经验:刚开始学JMM时,总觉得这些规则太抽象。后来调了几个并发bug,才真正理解。说白了,你只要记住:volatile保证可见性和有序性,但不保证原子性;synchronized三者都保证。面试时把这个说清楚,基本就过关了。

3.7 小结

这一章咱们把线程安全的三个核心问题讲清楚了:

  • 原子性:操作不可分割,用synchronized或Atomic类解决
  • 可见性:一个线程的修改对其他线程可见,用volatile或synchronized解决
  • 有序性:防止指令重排序带来的问题,用volatile或synchronized解决

而JMM,就是定义这些规则的基础。理解了JMM,你才能真正理解为什么volatile能保证可见性,为什么synchronized能保证原子性。下一章,咱们会深入volatile的底层实现,看看它到底是怎么工作的。

一句话总结:线程安全 = 原子性 + 可见性 + 有序性,而JMM就是这三者的理论基础。

公众号:蓝海资料掘金营,微信deep3321