线程安全之可见性(一)

一:举个栗子   

先举个例子:

public class ThreadVolidate {
    public static int i = 0;
    public static Boolean flag = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行了");
                while (flag) {
                    i++;
                }
                System.out.println("i== " + i);
            }
        });
        thread.start();
        Thread.sleep(3000);
        flag = false;
        System.out.println("shutdown......");
    }
}

       上述代码可以看到:在主线程main方法中,启动了一个子线程,子线程所做的事情,就是对i执行++操作,直到flag为false退出循环,输出i的值。主线程做的事情是,启动子线程,休眠3s后,把flag置为false,然后结束。

       经过上面的分析,来打印下执行结果:

       what?并没有打印出i的值,这是为什么呢?

       经过对代码的分析,可以得到应该会打印i的值,可是最后并没有,那么可以判断,子线程在执行的过程中,并没有退出while循环,这是为何?

二:延申分析

1:CPU高速缓存

       为了提高CPU的性能,CPU有高速缓存这么个东西,具体的概念可以去百度看下,对此我了解的并不深。

       看下上图:线程再去写或者读数据的过程中,会先去高速缓存中读写,在短时间内,主线程和子线程的数据可能会出现不一致的情况,但是呢,不会再3s这么长的时间,所以上面代码出现的问题,并不是高速缓存导致的,高速缓存并不愿意去背锅,那么是什么导致的呢?

2:指令重排

       在JVM执行字节码的过程中,会对字节码指令进行重排序,来提高执行的效率,但是要保证单线程内最后的执行结果不能发生改变,在多线程中,可能就会因为指令重排导致某个线程执行的结果发生错误;下面看下例子:

      上图中,假设线程1和线程2同时间执行,那么最终的结果是:线程1中:a=b,c=1;线程2中:d=c,e=f/2,假设c的初始值是0,那么线程2中d就是等于0,下面指令重排序后,会发生什么呢?

       假设上图是指令重排之后的结果:可以看到线程1进行了重排序,线程2还是原有顺序执行,可以看出,线程2中d的最终结果发生了变化,变成了1,这就是指令重排会导致的问题。

3:脚本代码和编译代码

脚本代码和编译代码是有着区别的,具体是什么呢?

1:脚本代码(解释执行):它会把代码,一行一行的翻译执行;

2:编译代码(编译执行):会把一整块代码,编译后执行。

而Java是介于脚本代码和编译代码之间的。

java源代码会被编译成class文件,用到的是执行前编译器,而运行时编译器,就是常说的JIT编译器,看下图:

       可以看到,JIT编译器中有解释执行和编译执行,当一个方法被多次调用或者方法中的循环体被多次循环,那么就会从解释执行转为编译执行。而我们知道在JVM的方法区中,有JIT编译后的代码,他会对这样的代码,进行优化,如上图编译执行指向的代码,这样他就把true这么一个值,缓存在里面了,那么就导致了上面代码没法打印出i的值,这就是上面代码出现问题的根本原因。

     怎么解决呢?在flag修饰符中加上volatile就可以解决了。

4:volatile关键字

 用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。

 

反编译之后可以看到,它有个ACC_VOLATILE标志。

  具体怎么实现:

  1:禁止缓存,也就是volatile修饰的变量禁止缓存,这样写入的时候就没有缓存了,那么CPU高速缓存导致的一点问题就可以解决了。

2:volatile修饰的,不能进行指令重排序,也就是说上面说的方法区缓存那一部分也没有了,这样就可以保证打印出i的值了:

public class ThreadVolidate {
    public static int i = 0;
    public static volatile Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行了");
                while (flag) {
                    i++;
                }
                System.out.println("i== " + i);
            }
        });
        thread.start();
        Thread.sleep(3000);
        flag = false;
        System.out.println("shutdown......");
    }
}

打印结果:

发布了21 篇原创文章 · 获赞 8 · 访问量 4367

猜你喜欢

转载自blog.csdn.net/qq_34365173/article/details/105073884