3、线程安全基础:什么是线程安全、原子性、可见性、有序性、Java内存模型(JMM)概述
好,咱们今天聊聊线程安全的基础。说实话,这章内容看着基础,但很多线上事故的根子就在这。我见过不少工作三五年的同事,聊起并发框架头头是道,但一问到JMM就含糊了。嗯,咱们先把地基打牢。
3.1 到底什么是线程安全?
线程安全这个概念,说白了就是:多个线程同时访问一个共享数据时,程序的行为仍然是正确的。你想想看,如果一段代码在单线程下跑得好好的,一到多线程就出幺蛾子,那它就是不安全的。
我个人习惯用一个简单的标准来判断:不管线程怎么调度、怎么交替执行,都不需要调用方做额外的同步措施,这个类就是线程安全的。
核心定义:当多个线程访问某个类时,这个类始终表现出正确的行为,那么它就是线程安全的。
我在项目中遇到过最典型的反例——一个计数器类,多个线程同时调用count++。结果呢?本该是1000的最终值,经常变成997、998。这就是典型的线程不安全。
3.2 原子性——要么全做,要么全不做
原子性,就是操作不可分割。一个操作要么全部执行完,要么一步都没执行。不会出现执行到一半被其他线程插一脚的情况。
举个例子,count++看起来是一条语句,但在CPU层面其实是三步:
- 从内存读取count的值
- 把值加1
- 把新值写回内存
这三步之间,随时可能被其他线程打断。这就是为什么最终结果不对。
避坑指南:我曾经在写一个库存扣减服务时,用了普通的int变量做扣减。压测时发现库存变成了负数。后来改成AtomicInteger,问题就解决了。记住:i++不是原子操作,AtomicInteger的incrementAndGet()才是。
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把内存分为主内存和工作内存:
| 内存区域 | 说明 |
|---|---|
| 主内存 | 所有线程共享,存储所有变量 |
| 工作内存 | 每个线程私有,存储主内存变量的副本 |
线程操作变量的流程:
- 从主内存拷贝变量到工作内存
- 在工作内存中执行操作
- 把结果写回主内存
嗯,这里要注意: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