美文网首页个人学习
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.线程安全之可见性

    可见性问题 代码 说明:在java运行参数加上server时候,为提高性能,会执行jit然后指令重排,导致实际上执...

  • 多线程之线程安全性

    多线程环境下使用非线程安全类会导致线程安全问题。线程安全问题表现为原子性,有序性,可见性 在讲述线程安全三大特性之...

  • 线程安全之可见性

    一:多线程中的问题 1.所见非所得 2.无法内眼去检查程序的准确性 3.不同的运行平台有不同的表现 4.错误很难重...

  • 身为JAVA工作者必须了解的实战知识(二)

    一、可见性 什么是可见性? Java线程安全需要防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且需要...

  • Java-可见性、原子性、有序性

    关键字:Java内存模型(JMM)、线程安全、可见性、原子性、有序性 1.线程安全(JMM) 多线程执行某个操作的...

  • 源码修炼笔记之ThreadLocal详解

    多线程线程安全的根源就是“共享”,即多个线程操作共享变量会引起可见性、原子性和顺序性的问题。解决线程安全首先我们想...

  • 线程安全-可见性

    共享变量在线程间不可见的原因 线程的交叉执行 重排序结合线程交叉执行 共享变量更新后的值没有在工作内存与主内存间及...

  • 多线程 | Volatile到底有什么用?

    Volatile的作用: 保持内存可见性.内存可见性:多个线程操作同一个变量,可确保写线程更新变量,其他读线程可以...

  • java多线程(壹)——线程与锁

    线程与线程安全问题 所有的线程安全问题都可以概况为三个点:原子性,可见性、有序性——————by Java多线程编...

  • volatile详解(二)(重排序)

    volatile保证线程安全可见性——volatile详解(一)[https://www.jianshu.com/...

网友评论

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

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