美文网首页个人学习
7.线程安全之可见性

7.线程安全之可见性

作者: 强某某 | 来源:发表于2020-03-03 15:07 被阅读0次

    可见性问题

    1. 代码
    // 关闭jit优化:-server -Djava.compiler=NONE
    
    //将运行模式设置为- server服务器端,就会变成死循环,默认idea运行时-client模式不会进行jvm层次的指令重排,也就是JIT时期的重排
    //通过设置JVM的参数,打印JIT编译的内容(这里说的编译非class文件,是底层汇编内容),通过可视化工具jitwatch查看
    //-server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=jit.log
    public class VisibilityDemo {
        private  boolean flag=true;
        public static void main(String[] args) throws InterruptedException {
            VisibilityDemo demo=new VisibilityDemo();
            new Thread(()->{
                int i=0;
                //class->运行时jit编译->汇编指令->重排序
                while (demo.flag) {//指令重排序导致死循环
                        i++;
                }
    //            if (demo.flag) {
    //                while (true) {
    //                    i++;
    //                }
    //            }
                System.out.println(i);
            }).start();
    
            TimeUnit.SECONDS.sleep(2);
            demo.flag=false;
            System.out.println("重置了");
        }
    }
    
    

    说明:在java运行参数加上server时候,为提高性能,会执行jit然后指令重排,导致实际上执行的是注释的代码,从而导致死循环

    1. 工作内存缓存

    其实也就是cpu缓存,但是实际上该缓存只会让另外线程晚一点发现变量已经改为false了,而不是导致死循环


    1.png
    1. 指令重排
      出了CPU会进行指令重排,Java编程语言的语义允许编译器和微处理器执行优化,这些优化可以与不正确的同步代码交互,从而产生看似矛盾的行为。


      2.png

    从上图可知,左边的重排就会导致结果的未可知。

    3.png
    1. 共享变量描述
      可以在线程之间共享的内存称为共享内存或堆内存。例如:实例字段,静态字段和数组元素都存储在堆内存中。
    1. Happens-before先行发生原则:下列的时候jit不进行指令重排,同时jit会调用cpu的内存屏障禁止cpu级别的指令重排
      happens-before关系主要用于强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。
      具体虚拟机的实现,有必要确保以下原则的成立
    • 某个线程中的每个动作都happens-before该线程中该动作后面的动作
    • 某个管程(监视器)上的unlock动作happens-before同一个管程上后续的lock动作()
    • 对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作
    • 对某个线程对象上调用start()方法happens-before该启动了的线程中的任意动作
    • 某个线程中的所有动作happens-before任意其他线程成功从该线程对象上的join()返回
    • 如果某个动作a happens-before动作 b,且b happens-before动作c,则有 a happens-before c

    管程概念补充

    1.     管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
    
    2.     进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。
    
    3.     在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。
    
    

    针对第二条:如下加上同步关键字则jit就不会指令重排,不会死循环

    public static void main(String[] args) throws InterruptedException {
            VisibilityDemo demo=new VisibilityDemo();
            new Thread(()->{
                int i=0;
                //class->运行时jit编译->汇编指令->重排序
                while (demo.flag) {//指令重排序导致死循环
                   synchronized (this) {
                        i++;
                   }
                }
                System.out.println(i);
            }).start();
    
            TimeUnit.SECONDS.sleep(2);
            demo.flag=false;
            System.out.println("重置了");
        }
    
    1. volatile关键字
      可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到

    根据JMM中规定的happen before和同步原则:
    对某个volatile字段的写操作happens-before每个后续对该volatile字段的读操作。
    对volatile变量的v的写入,与所有其他线程后续对v的读同步

    • 要满足这些条件,所以volatile关键字有以下功能
      • 禁止缓存(所有缓存,不但指cpu缓存,甚至包含jvm的缓存等等):volatile变量的访问控制符会加个ACC_VOLATILE(禁止缓存can not be cache)
      • 对volatile变量相关的指令不做重排序

    针对第三条:加上volatile关键字也禁止了指令重排,所以也可以避免死循环

    public class VisibilityDemo {
        private volatile boolean flag=true;
        public static void main(String[] args) throws InterruptedException {
            VisibilityDemo demo=new VisibilityDemo();
            new Thread(()->{
                int i=0;
                //class->运行时jit编译->汇编指令->重排序
                while (demo.flag) {//指令重排序导致死循环
                        i++;
                }
                System.out.println(i);
            }).start();
    
            TimeUnit.SECONDS.sleep(2);
            demo.flag=false;
            System.out.println("重置了");
        }
    }
    

    说明:volatile修饰的关键字,则没有缓存的说法了,每次其他线程或者本线程读取的时候都会从主内存读取,而且修饰之后,jit也不会对这部分进行指令重排,从而正常结束

    1. word tearing字节处理


      4.png
    1. double和long的特殊处理


      5.png
    1. final在JMM中处理
    • final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。伪代码示例:f=new finalDemo();读取到的f.x一定时最新的,x为final字段。如果不是final修饰的话,则不能保证多线程读取时候初始一定是初始化之后的值,偶尔可以看到默认值
    • 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值
    public class Final {
        final int x;
        int y;
    
        public Final(){
             x=1;
             y=x;
            System.out.println(y);
        }
    
        public static void main(String[] args) {
            Final f=new Final();
        }
    }
    

    如果没设置final修饰x,则可能出现y是默认值的情况,如果加了final修饰那就肯定是1

    • 读取该共享对象的final成员变量之前,先要读取共享对象。
      伪代码:r=new ReferenceObj();k=r.f;这两个操作不能重排序
    • 通常static final是不可以修改的字段,然后System.in,System.out和System.err是static final字段,历史遗留原因,可以通过set方法改变,称这些字段为写保护,以区别于普通final字段

    相关文章

      网友评论

        本文标题:7.线程安全之可见性

        本文链接:https://www.haomeiwen.com/subject/bfchlhtx.html