可见性和有序性问题,是由于CPU缓存和编译优化导致的,
按需禁用缓存和编译优化,既解决了并发问题,又保证了基本的性能优化。
使用volatile
禁用CPU缓存
使用 volatile
描述的变量,编译器会直接操作内存,而不是从缓存中存取值。
Happens-Before 规则 增强了 volatile
修饰的变量
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
假设线程 A 执行 writer()
方法,按照 volatile 语义,会把变量 v=true
写入内存;
假设线程 B 执行 reader()
方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v
;
如果线程 B 看到 v == true
时,那么线程 B 看到的变量 x
是多少呢?
在1.5以下版本中,可能是 42
也可能是 0
(x
并没有被 volatile
修饰,可能会从缓存中取值)。
在1.5及以上版本中,由于引入了 Happens-Before 原则 x
的值则为 42
。
Happens-Before 原则是指:前面一个操作的结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
顺序性规则
按照代码的顺序,前面的操作相对于后面的操作是可见的。
volatile 变量规则
一个 volatile 变量的写操作, 相对于后续对这个 volatile 变量的读操作是可见的。
传递性规则
如果 A 相对于 B 是可见的,且 B 相对于 C 是可见的,那么 A 相对于 C 也是可见的。
由此可见:
- 根据顺序性规则:
x=42
相对于 写变量v=true
是可见的; - 根据volatile变量规则:写变量
v=true
相对于 读变量v=true
是可见的。 - 根据传递性规则:
x=42
相对于 读变量v=true
是可见的。
如果线程 B 读到了 v=true
,那么线程 A 设置的 x=42
对线程 B 是可见的。
管程中锁的规则
一个线程对一个锁临界区执行完解锁后,变量的值对于其他线程后续对这个临界区做加锁操作是可见的。
synchronized
就是Java中管程的实现。
synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁
假设 x
的初始值是 10;
线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁);
线程 B 进入代码块时,能够看到线程 A 对 x
的写操作,也就是线程 B 能够看到 x==12
。
线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
线程 join() 规则
如果在线程 A 中,调用线程 B 的 join()
并成功返回,那么线程 B 中的任意操作 相对于该 join()
操作的返回 是可见的。
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66