环境
OS | Win10 |
CPU | 4 核 8 线程 |
IDE | IntelliJ IDEA 2019.3 |
JDK | 1.8 -server 模式 |
场景
最初的代码
一个线程 A 根据 flag 的值执行死循环, 另一个线程 B 只执行一行代码, 修改 flag 的值, 让 A 线程死循环终止.
Visbility.java
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- flag = true;
- }
- }
Main.java
- public class Main {
- public static void main(String[] args) {
- Visbility visbility = new Visbility();
- Thread cyclic = new Thread(visbility::cyclic);
- Thread setter = new Thread(visbility::setter);
- cyclic.start();
- setter.start();
- }
- }
多次执行 Main 函数结果: 程序很快就终止.
这是为什么呢? 我没有让 flag 值在多线程之间内存可见呀, 怎么线程 setter 修改 flag 后, cyclic 线程获得了修改后的 flag 终止死循环? 先带着疑问.
添加 for 循环耗时代码
接着, 在 setter 方法里, 在修改该 flag 之前, 添加一行耗时代码 (用 for 循环, 为什么不用 TimeUnit, 后面会说到), 此时 Visbility.java 如下:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i <999999; i++) ;
- flag = true;
- }
- }
多次执行 Main 函数结果: 程序一直不结束.
这是为什么呢? 难道执行个循环 99999 次, CPU 永远执行不完导致 flag 的值无法被修改该吗? 还是说内存可见性的问题?
用 volatile 解决内存可见性
我们给 flag 加上 volatile 关键字进行修饰 (后面有其他的方式如锁, System.out.println -_- 解决变量内存及时可见性),Visibility.java 代码如下:
- public class Visbility {
- private volatile boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i < 999999; i++) ;
- flag = true;
- }
- }
多次执行 Main 函数结果: 程序几百毫秒后终止.
看来确实存在内存可见性的问题, 线程 cyclic 获取到了 setter 线程修改后的 flag 并终止, 解决内存可见性的方式特别多, 后面再列几种;
但是结果证明了, 并不是 CPU 执行不完了 999999 次的循环, 而且是很快的执行完, 那为什么和最初什么都没加的代码相比, 加上了这 99999 次循环的耗时, 就必须要加上 volatile 才能让 setter 线程中的 flag 的值被 cyclic 线程感知.
去掉 volatile, 减少 for 循环次数, 减少耗时
继续修改代码, 去掉 volatile, 并把 for 循环的次数 999999 减少至 99999(大家不同的机器不同的环境可能需要设置不同数值),Visbility.java 代码如下:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- for (int i = 0; i < 99999; i++) ;
- flag = true;
- }
- }
多次执行 Main 函数结果: 程序几百毫秒内结束.
这里我去掉了 volatile 关键字, 仅仅减少了 setter 线程修改 flag 之前模拟的 for 循环耗时, 结果似乎又 flag 内存可见了 (cyclic 死循环线程终止).
总结上面的几中情况
当 setter 线程修改 flag 之前无任务和耗时相对较短的任务时, 不需要 volatile 修饰 flag 变量, cyclic 线程能获得被 setter 修改该后的 flag 值;
当 setter 线程修改该 flag 之前有耗时相对较长的任务时, 需要 volatile 修改 flag 变量, cyclic 线程才能获得被 setter 修改该后的 flag 值.
几种猜想 (暂未证明)
1. 在皮秒级内 (这也是为什么我这里模拟耗时用 for 循环, 而不用 TimeUnit, 因为 TimeUnit 最小的单位是纳秒, 开始我使用最小的单位时间 TimeUnit.NANOSECONDS.sleep(1), 多次执行程序, 每次结果都是一直都不结束, 所以我需要更小的耗时时间),JVM 已经感知到 "flag" 被修改, 所以两个线程都获取的主存的值, 第一个线程的循环终止
2. 由于 setter 线程的任务实在是太小 (联想到了进程调度算法), 所以 setter 在极短时间内被 CPU 执行完后, 线程 cyclic 也立刻被同一个 CPU 执行, 即取的是同一块本地内存 (CPU 高速缓存)
3. 由于 setter 线程的任务实在是太小 (联想到了进程调度算法), 所以 setter 在极短时间内被 CPU 执行完后, 值已经被刷新到主存, cyclic 获得的是主存中最新的值
本来想验证下第二种猜想, 查了下, 暂时无法简单的通过 Java 类库代码来获取当前线程是被哪个 CPU 执行 (JNA + 本地安装对应的 Library:https://github.com/OpenHFT/Java-Thread-Affinity);
耗时任务的意义
有了这个耗时任务, 如果上面的 cyclic 已经启动了, JVM 感知到 (在耗时任务执行过程中, CPU 早已做了多次运算了), 除了 cyclic 这个线程以外, 没有其他线程在操作 "flag", JVM 会假设 "flag" 的值一直都没有被改变, 所以 cyclic 线程一直从自身线程本地内存中获取值 (在未使用 synchronized, volatile 等实现 "flag" 的内存可见性时) , 所以就算 setter 线程修改 "flag" 的值, cyclic 还是从自己的线程的本地内存中读取.
如何保证变量在内存中及时可见?
主要有两种, 一种是用 volatile, 一种是锁;
还有 Atomic Class? 底层 value 也是用的 volatile, 以及 sun.misc.Unsafe:https://www.cnblogs.com/theRhyme/p/12129120.html;
当然 AQS 也是 volatile+sun.misc.Unsase.
Volatile 保证变量在内存中及时可见
至于 volatile 例子上面已经写了, JAVA 内存模型中 VOLATILE 关键字的作用: https://www.cnblogs.com/theRhyme/p/9396834.html
用锁来保证内存的可见性
锁有很多很多种, 所以实现的方式也有很多, 这里列几种有趣的实现, 比如 System.out.println 也能保证能保证内存可见性?
System.out.println 的形式
首先我们把 setter 修改 flag 之前添加耗时任务 (仅 66 纳秒)TimeUnit.NANOSECONDS.sleep(66), 即确保不触发刚才的猜想:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
执行结果和之前一样: 多次执行 Main 函数, 每次都不结束.
然后我们在 cyclic 死循环里添加一行输出语句: System.out.println, 不加 volatile 关键字修饰 flag, 此时 Visibility.java 如下:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- System.out.println(flag);
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
多次执行 Main 函数的结果: 都是输出了几十个 false 后程序终止.
什么情况, 这里没有用 volatile 修饰 flag 啊, 也没用锁啊;
真的没用锁吗? println 源码如下:
- public void println(boolean x) {
- synchronized (this) {
- print(x);
- newLine();
- }
- }
原来是锁住了 this 对象, 即 out 属性的实例, 所以我们在这个场景里用锁的形式保证变量内存及时可见甚至可以是下面这样:
- import java.util.concurrent.TimeUnit;
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- System.out.println();
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
甚至还可以这样:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!flag){
- synchronized ("123"){
- }
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
但是不能这样:
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- synchronized ("123"){
- }
- while (!flag){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- flag = true;
- }
- }
正常用锁的方式
还是写点正常点的代码吧... 也是最基础的例子
- public class Visbility {
- private boolean flag;
- public void cyclic(){
- while (!isFlag()){
- }
- }
- public void setter(){
- try {
- TimeUnit.NANOSECONDS.sleep(66);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- setFlag(true);
- }
- public synchronized boolean isFlag() {
- return flag;
- }
- public synchronized void setFlag(boolean flag) {
- this.flag = flag;
- }
- }
在这个场景中, 用锁的方式大同小异, 不管是用 wait-notifyAll, 还是 lock*,await-signallAll, 亦或是, countdown,await,take,put 等方法 , 都是在用锁而已.
对 DCL 单例模式的思考
在 DCL 单例中, 既然锁 synchronized 能保证原子性和可见性, 那 volatile 的作用是什么呢? volatile 起的作用是禁止指令重排序和可见性.
- public class DoubleCheckedLocking {
- private volatile static DoubleCheckedLocking dcl = null;
- private DoubleCheckedLocking() {
- }
- public static DoubleCheckedLocking getInstance() {
- if (dcl == null) {
- synchronized (DoubleCheckedLocking.class) {
- if (dcl == null) {
- dcl = new DoubleCheckedLocking();
- }
- }
- }
- return dcl;
- }
- }
对于 "dcl = new DoubleCheckedLocking();" 这行代码, 首先 DoubleCheckedLocking.java 被编译成字节码, 然后被类加载器加载, 接着还有下面 3 步骤:
- memory = allocate(); // 1. 分配内存空间
- init(memory); // 2. 将对象初始化
- dcl = memory;// 3. 设置 dcl 指向刚分配的内存地址, 此时 dcl != null
step2 和 step3 在单线程环境下允许指令重排, 即先把未初始化的内存地址指向 dcl(此时 dcl!=null), 然后才把内存空间初始化;
但是如果在多线程的环境下, JVM 优化指令重排后执行顺序如果是 step1->step3->step2,A 线程执行到 step3 此时还未执行 step2 对象还未初始化, 但是此时 dcl 已经被赋值为 memory, 所以 dcl!=null, 同时另一个线程 B 执行最外层代码块 if(dcl==null 结果为 false), 就直接 return 未被初始化的错误的 dcl.
来源: https://www.cnblogs.com/theRhyme/p/12145461.html