Juc篇-线程安全问题引入(从i++问题的底层出发)
在多线程编程中,线程安全是一个绕不开的核心概念。无论是业务代码的共享变量,还是系统级别的缓存、队列、对象状态管理,都需要思考:在并发环境下,代码还能否按照预期执行?
本文将从线程安全的本质开始,结合经典的 i++ 问题深入分析其本质原因,并总结 Java 中常用的三大解决手段(synchronized / Lock / ThreadLocal)的区别与适用场景。
一、什么是线程安全?
线程安全的本质是:
多个线程并发访问共享资源时,最终结果必须符合预期,不出现数据错误或逻辑混乱。
这背后有三个根源性问题:
1. 原子性(Atomicity)
操作是否不可分割?
例如:i++ 虽然是“一行代码”,但绝不是原子操作。
2. 可见性(Visibility)
一个线程修改变量后,其他线程能否立即看到?
例如:线程 A 改了 flag=true,线程 B 却一直看不到,造成死循环。
3. 有序性(Ordering)
代码实际执行顺序是否被重排序?
例如写入变量后立即读取,但 CPU 可能会优化导致顺序变化。
只要三个问题之一出现隐患,就会导致线程不安全。
二、经典案例:为什么 i++ 在并发下是不安全的?
许多人以为 i++ 是简单语句,但 JVM 实际会将其拆解为多条字节码指令。
示例代码:
i++;
对应字节码流程如下:
getstatic i // 读取静态变量 i
iconst_1 // 放常量 1
iadd // 执行加法 (i+1)
putstatic i // 写回静态变量 i
总共 4 步 —— 并非原子性!
为什么会出问题?
多线程执行时,线程切换可能发生在任何一步。
假设初始值 i = 0,线程 A 和线程 B 执行 i++:
线程 A:读取到 i=0
线程 B:也读取到 i=0
A 计算结果 1
B 计算结果 1
A 写回 1
B 写回 1
最终结果:i = 1(预期为 2)。
并发问题就出现了。
三、如何解决 i++ 的线程安全问题?
Java 提供三大类手段:
1. synchronized(JVM 关键字)
提供 原子性、可见性、有序性
通过对象锁实现互斥
写法简单、可靠,但阻塞开销较大
示例:
synchronized (this) {i++;
}
2. Lock(显式锁,如 ReentrantLock)
相比于 synchronized 的优势:
✔ 能中断
✔ 可超时
✔ 可以实现公平锁
✔ 支持条件变量
✔ API 更灵活
示例:
lock.lock();
try {i++;
} finally {lock.unlock();
}
3. 原子类(AtomicInteger 等)
基于 CAS(无锁方案),速度最快。
AtomicInteger i = new AtomicInteger();
i.incrementAndGet(); // 原子操作
四、ThreadLocal 到底是什么?它为什么不提供线程安全?
很多初学者以为 ThreadLocal 是“锁”或“线程安全工具”,其实不是。
ThreadLocal 的核心思想是:
让每个线程拥有变量的一个独立副本。避免共享,从而避免线程安全问题。
它不是保护共享资源,而是让每个线程独占数据。
典型使用场景:
每个线程独立的日期格式化器
每个线程独立的数据库连接
登录信息上下文(每线程独立 session)
ThreadLocal 不保证原子性/可见性,只是绕开共享。
五、三者之间的区别总结(核心对比)
| 特性 | synchronized | Lock(ReentrantLock) | ThreadLocal |
|---|---|---|---|
| 目的 | 保证共享数据互斥 | 更灵活的互斥控制 | 每线程独占数据 |
| 原理 | JVM 内置锁 | 代码级锁,底层 AQS | 每线程内部 Map 存副本 |
| 是否阻塞 | 是 | 是(可中断/可超时) | 不阻塞 |
| 是否保证线程安全 | 是 | 是 | 本质上不保证,只是避免共享 |
| 使用复杂度 | 低 | 中 | 低 |
| 性能 | 中 | 中~高 | 高 |
| 适用场景 | 简单同步 | 复杂同步 | 不共享的线程本地变量 |
一句话总结:
synchronized:简单粗暴、安全
Lock:可控性强,适合复杂并发场景
ThreadLocal:不是锁,通过“避免共享”间接避免线程安全问题
六、整体的并发编程理解框架(送你一张图)
线程安全问题的本质在于:多个线程对共享资源的并发读写会导致指令交错,从而破坏原子性、可见性、有序性。
要么“锁住共享资源”(synchronized/Lock/原子类),
要么“避免共享”(ThreadLocal),
本质上都是围绕这三大问题进行解决。
整体框架如下:
线程安全本质问题├── 原子性问题 → i++ 被拆成 4 步├── 可见性问题 → 缓存导致线程读取旧值└── 有序性问题 → CPU/编译器重排序解决方案思路├── 保证互斥:synchronized / Lock├── 保证可见性:volatile(部分场景)├── 保证原子性:AtomicXX 原子类└── 避免共享:ThreadLocal
七、总结
线程安全并不是某个固定的 API 或语言特性,而是一个由底层 CPU、JVM、内存模型共同决定的系统级问题。
关键记住三点:
✔ 线程安全的根源:原子性、可见性、有序性
✔ i++ 在并发下是典型的非原子操作
✔ Java 中处理线程安全的三大手段各有定位
synchronized:语言层面,简单可靠
Lock:更灵活的锁
ThreadLocal:不是锁,而是避免共享
理解这些,你就掌握了 Java 并发编程的核心基础。
