07:线程安全-可见性问题

由指令重排序引起的可见性问题:
public class Test {
    // 如果运行时加上 -server 下面的代码就变成了死循环,没有加就正常运行。(运行器的编译优化只有在服务器模式下才执行)
    // 通过设置JVM参数,打印出JIT(即时编译)编译的内容(这里说的编译不是指class文件的编译,而是指未变级别的编译)
    private boolean flag = true;
   // -server -Djava.compiler=NONE 参数可以关闭jit优化。
    // 在多线程中,由于指令重排序引起的线程可见性问题。
    public static void main(String[] args) throws IOException, InterruptedException {
        Test demo1 = new Test();
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                // class文件在运行时jit编译成为汇编指令,汇编指令出现了重排序。
                /*
                // 重排序后的逻辑。因为while语句里面需要一直判断flag。所以jvm优化为外层使用if判断一次。
                if(demo1.flag){
                    while (true){
                        i++;
                    }
                }  hot code : 热点代码。(就像是下面的while语句中的内容,执行频率很高。jvm认为是热点代码)
                */
                while (demo1.flag) {
                    i++;
                }
                System.out.println("i = " + i);
            }
        }).start();
        TimeUnit.SECONDS.sleep(2);
        // 设置flag为false,使上面的线程结束循环。
        demo1.flag = false;
        System.out.println("主线程结束");
    }
}
问题描述:
    多个CPU执行多个线程任务时,由于线程栈是独享的,如果共享数据在缓存中更新不及时,会出现多线程共享数据不一致。(基本不可能出现,因为现代计算机比较完善了)    
    运行时编译器JIT,将class编译称为汇编指令。在服务器模式下。会对执行的顺序进行优化重排序。但是放在多线程中执行时会由于执行的顺序改变执行的结果。
    由于指令进行了重排序。导致缓存的修改对本线程不可见。
    (指令重排序导致了while语句变成了if执行,之后在多线程的情况下。一个线程先判断if,另外一个线程后修改条件。所以if语句中的内容不会执行了。)
    (指令重排序是为了:提高CPU的使用率。一个线程阻塞的时候,别的线程继续执行。)
Java官方文档:docs.oracle.com
Java内存模型
什么是内存模型:
内存模型通过限定程序在某个点上是否可以读取某个值。内存模型用于描述程序运行的可能行为。从而可以根据内存模型对程序的运行结果进行准确的推断。
Java内存模型的规范:(规范Jvm的重排序规则 )
1:规范线程数据的存放:
可以在线程之间共享的内存称为:共享内存或堆内存。(实例、静态字段、数组元素等)
2:规范线程之间的操作:
写数据、读数据、锁定、解锁、线程启动、线程终止、外部操作(socket操作等)
3:规范线程同步规则:
对同一把锁的解锁和加锁同步(就是说解锁后才可以加锁)、
对volatile变量的写入和读取要同步(就是说,一个线程的写入之后别的才可以读到)、
线程的启动操作和线程中的第一个操作同步(就是说:启动之后才可以执行操作)、
等等等。
都是为了保证数据可见性和一致性。
4:Happens-before先行发生原则:强调两个有冲突动作的顺序,定义数据使用的时机。(我做的操作,对你是可见的。)
某个管程上的unlock动作Happens-before同一个管程上的lock动作。(线程解锁了才能加锁)、
某个线程对volatile字段的操作,对后续代码都可见、
某个线程中的动作都Happens-before该线程后面的所有动作。(就是说线程的动作都知道前面执行了什么。)、
等等。
volatile关键字:
加了volatile关键字的共享变量,会遵循happens-before规范、Jvm不会指令重排序、禁止缓存该变量。对该关键字的修改会可见于别的线程。
没有缓存后,直接读取主存中的数据,所以各个线程之间数据是可见的。
final在JVM中的处理:
用在类上:是一个终极类。
构造函数中设置final字段,只要别的线程可以获取到该对象,那么final字段一定是正确的。(如果不final,可能读到默认值。)
如果在构造方法中set字段,然后后get该字段,如果该字段不是final的。有可能读取到默认值。
例如:public finalDemo(x =1 ; y = x); 如果x是final的,y将=1;否则y可能为0;
读取共享的final成员变量之前,要先获取到共享对象。这个操作不能重排序。
了解内容:
static final 是不可修改的字段。然而System.in、System.out、System.err都是static final的字段。但是可以通过set方法修改。(历史遗留原因)
虚拟机规范里double类型的数据不是原子性的。例如读:先读32位,再读32位。但是 final double是原子的。(商业的jvm考虑到实际情况,一般多将double做成了原子的)

原文地址:https://www.cnblogs.com/Xmingzi/p/12601067.html