上述代码可能会有2个问题,1、内存可见性。 2、指令重排序
什么是内存可见性
Java 内存模型
JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。
这样当线程三修改A的共享变量时,并没有及时的更新到主内存中,线程一再读取的时候拿到的就是没有更新过的变量A
我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。
1、加锁
2、使用volatile关键字。
这里我们使用了加锁,所以不会有内存可见性问题。volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
那么这里我们为什么不使用volatile呢,毕竟使用volatile成本更低,这里就涉及到了volatile的原子性问题。它不能像synchronized能够保证操作的原子性。再多线程环境下,使用 volatile 修饰的变量是线程不安全的。
什么是指令重排序
上述的doucleCheck = new DoucleCheck();代码有问题:
其底层会分为三个操作:
1. 分配一块内存。
2. 在内存上初始化成员变量。
3. 把doucleCheck 引用指向内存。
在这三个操作中,操作2和操作3可能重排序,即先把doucleCheck 指向内存,再初始化成员变量,因为 二者并没有先后的依赖关系。此时,另外一个线程可能拿到一个未完全初始化的对象。这时,直接访问 里面的成员变量,就可能出错。这就是典型的“构造方法溢出”问题。
解决办法也很简单,就是为doucleCheck 变量加上volatile修饰。 volatile的三重功效:64位写入的原子性、内存可见性和禁止重排序。
volatile为什么可以禁止指令重排序呢
这里就涉及到jmm的happen-before规则
JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
如果A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。A happen before B不代表A一定在B之前执行。因为,对于多线程程序而言,两个操作的执行顺序是不确 定的。happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。定义了内存可见性的约 束,也就定义了一系列重排序的约束。
所以上述代码改为
4.3.3 happen-before规则总结 1. 单线程中的每个操作,happen-before于该线程中任意后续操作。 2. 对volatile变量的写,happen-before于后续对这个变量的读。 3. 对synchronized的解锁,happen-before于后续对这个锁的加锁。 4. 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的 读。 四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外 的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。
happen-before规则总结
1. 单线程中的每个操作,happen-before于该线程中任意后续操作。
2. 对volatile变量的写,happen-before于后续对这个变量的读。
3. 对synchronized的解锁,happen-before于后续对这个锁的加锁。
4. 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的 读。
四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外 的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。