并发编程bug的源头
- 可见性: 一个线程对共享变量的修改,另外一个线程能够立刻看到;
- 原子性;我们把一个或者多个操作在CPU执行的过程中不被中断的特性,称为原子性;
- 有序性;编译优化带来的有序问题;
为了合理利用CPU资源,操作系统和缓存等做出的贡献;
- CPU 增加了缓存,以均衡与内存的速度差异;
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。
Java内存模型
- 规范了JVM如何提供按需禁用缓存和编译优化的方法;
Happens-Before规则
前面一个操作的结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽然允许编译器的优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
- 程序的顺序性规则:这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作;
- volatile变量规则: 这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作;
- 传递性:这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C;
- 管程中锁的规则: 管程是计算机中的同步原语;这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
- 线程的start规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
- 线程的join规则:这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作
逸出
在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的。因此我们一定要避免“逸出”。
// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲 this 逸出,
global.obj = this;
}
解决原子性操作- 锁技术:synchronized
- 当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
- 当修饰非静态方法的时候,锁定的是当前实例对象 this。
class X {
// 修饰静态方法
synchronized(X.class) static void bar() {
// 临界区
}
}
class X {
// 修饰非静态方法
synchronized(this) void foo() {
// 临界区
}
}