缓存问题会导致线程之间出现可见性的问题,指令重排会导致有序性问题,如何解决呢?最简单的方式就是禁用缓存和禁止指令重排。
我们可以通过 final,volatile,sycchronized关键字和happens-Before原则。

Volatile

volatile最早在C语言中就有,最初始的含义就是禁用CPU缓存。

  1. class VolatileExample {
  2. int x = 0;
  3. volatile boolean v = false;
  4. public void writer() {
  5. x = 42;
  6. v = true;
  7. }
  8. public void reader() {
  9. if (v == true) {
  10. // 这里x会是多少呢?
  11. }
  12. }
  13. }

假设有这么一段代码,x的值是多少呢?考虑到cpu缓存,x可能是42 也可能是0,但在Java1.5版本以后,它一定是42.

happens-Before规则

1. 程序顺序性规则

简单来讲就是,操作对后可见,前面的操作happens-before后续操作,x = 42 是一定发生在 v = true之前的。
避免发生指令重排,x = 42优先v = true的情况出现

2.volatile 变量规则

volatile的写操作一定在volatile读之前,且具有传递性。

3.传递性规则

例如线程B读到了 v == true这一行,那么 v = true 一定是先执行的,且x = 42 也一定是在 v = true之前的。

4.管程中锁的规则

管程 是通用的同步源句,Java中通过Synchronize实现。

  1. synchronized (this) { //此处自动加锁
  2. // x是共享变量,初始值=10
  3. if (this.x < 12) {
  4. this.x = 12;
  5. }
  6. } //此处自动解锁

Java 中 会自动加锁,自动解锁。加锁和解锁这个区域称作临界区。
这个规则是:线程A执行了这个临界区的操作,x = 12,那么线程B 进入代码块后,能够看到 x = 12.

5.线程start()原则

线程A启动子线程B后,子线程B能够看到线程A的所有操作。
也就是说,start操作前的操作对start后操作都是可见的。

6.Join规则

也就是说主线程A等待B线程操作完成,join前的所有操作线程A都是可见的。

final 关键字

final关键字修饰的是不可变,指令可以随意的优化,反正都是不可变的,但需要注意对象逃逸。
什么是逃逸?
例如,错误的构造方法,在对象初始化的时候,把内部的值,交给了另外的对象,该值就逃逸了。
ex:

  1. // 以下代码来源于【参考1】
  2. final int x;
  3. // 错误的构造函数
  4. public FinalFieldExample() {
  5. x = 3;
  6. y = 4;
  7. // 此处就是讲this逸出,
  8. global.obj = this;
  9. }

global.obj 能拿到x 的值,可能为 0 。