Java并发编程实战笔记
第一部分:并发编程Bug的根源
1. 硬件层面的矛盾
- CPU、内存、I/O设备的速度差异
- CPU vs 内存:一天 vs 一年
- 内存 vs I/O:一天 vs 十年
- 系统的应对方案:
- CPU增加缓存
- 操作系统增加进程、线程
- 编译程序优化指令执行顺序
2. 并发编程的三大问题
2.1 可见性问题
- 定义:一个线程对共享变量的修改,其他线程无法立即看到
- 根源:CPU缓存导致
- 示例:多核场景下的计数器问题
class Test {
private long count = 0;
void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
}
2.2 原子性问题
- 定义:一个操作不能被线程调度机制中断的特性
- 根源:线程切换
- CPU指令级别的原子性:
- 一条普通指令可能需要多条CPU指令完成
- 线程切换可能发生在任何一条CPU指令后
2.3 有序性问题
- 定义:程序执行顺序与代码编写顺序不一致
- 根源:编译器优化
- 案例:DCL(双重检查锁定)单例模式的隐患
第二部分:Java内存模型的解决方案
1. Java内存模型(JMM)的设计原则
- 程序员可以自主选择是否禁用缓存和编译优化
- 需要提供简单易用的禁用方法
- 兼顾性能和正确性
2. 核心技术方案
2.1 volatile关键字
- 功能:禁用CPU缓存
- 1.5版本增强:
- 不仅保证可见性
- 还能保证部分有序性
- 适用场景:
- 单个共享变量的读写
- 对变量的写操作不依赖当前值
2.2 Happens-Before规则
六大规则:
-
程序顺序规则
- 单线程内代码前后顺序可见性保证
-
volatile变量规则
- 写volatile变量对后续读volatile变量可见
-
传递性规则
- A Happens-Before B
- B Happens-Before C
- 则A Happens-Before C
-
管程中锁规则(synchronized)
- 解锁Happens-Before加锁
-
线程start()规则
- start()前对变量的修改对新线程可见
-
线程join()规则
- 子线程的操作对join()之后的主线程可见
2.3 final关键字
- 作用:告诉编译器这是不可变量
- 注意事项:
- 1.5版本后增强了重排序约束
- 避免构造函数中this引用逸出
3. 实践指南
3.1 选择合适的方案
- volatile:适用于单个变量的可见性
- synchronized:适用于复合操作的原子性
- Happens-Before:理解跨线程的可见性保证
3.2 代码实践建议
- 优先使用成熟的工具类
- 避免过度使用volatile
- 理解happens-before对可见性的保证
- 合理使用final提升性能
思考题
Q: 如何确保一个线程写入的变量对其他线程可见?
A: 以下几种方式:
- 使用volatile修饰变量
- 使用synchronized同步
- 使用显式锁(Lock)
- 利用happens-before规则(如线程start/join)