目录

一、内存可见性

1.1 Java内存模型(JMM)

1.2 内存可见性演示

二、指令重排序

三、关键字 volatile 


一、内存可见性

1.1 Java内存模型(JMM)

1)什么是Java内存模型(JMM)?
Java内存模型即Java Memory Model,简称JMM。用于屏蔽各种硬件操作系统的内存访问差异,以实现让Java程序在各平台下都能够达到一致的内存访问效果,即实现跨平台”。
2)JMM中的“主内存”概念和“工作内存”概念
“主内存” 硬件中的内存,在JMM中表述为“主内存”,其中存储线程间的共享变量数据
工作内存” CPU寄存器缓存临时存储区,在JMM中表述为“工作内存”,每个线程都有自己的“工作内存”。
3)线程和“主内存”、“工作内存”的关系

线程需要读取共享变量时,会从“主内存”拷贝至“工作内存”,再从“工作内存”读取

线程需要修改共享变量时,会先修改工作内存”中的数据副本,再将数据同步回“主内存”。

线程运行中,数据的交互是频繁且持续的,而CPU访问自身寄存器和高速缓存速度远高于访问内存的速度

因此,采用频繁与“工作内存”交互需要时再与“主内存”交互的工作策略,有利于提高运行效率编译器优化方式之一

1.2 内存可见性演示

什么是内存可见性?

内存可见性是指,线程共享变量值修改,能否被其他线程及时察觉。

如果一个线程修改共享变量值,但没有及时写回内存中,导致其他线程无法获得已修改正确数据,这就被认为出现了线程安全问题

内存可见性是导致线程不安全原因之一。

代码演示内存不可见导致线程不安全

public class Volatile_Demo0 {
    //有一个共享变量flag,注意该变量没有volatile 修饰public static int flag = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建一个线程,线程中当flag为0时,一直循环判断;
        Thread t = new Thread(()->{
            while (flag == 0){}
        });

        //启动线程;
        System.out.println("run开始");
        t.start();

        //让main线程休眠两秒后,将flag的值改为1;
        Thread.sleep(2000);
        flag = 1;

        //让main线程等待t线程结束t.join();
        System.out.println("run结束");
    }
}

//运行结果run开始
...

程序一直在执行没有打印run结束”。
出现了线程安全问题

上述代码问题分析

程序无法结束原因什么

根据代码,flag 在线启动两秒后被改为 1 ,此时 t 线程应该因为跳出 while 循环执行完毕。

但实际情况却不是这样,t 线程没有结束

正如上文“线程和‘主内存’、‘工作内存’的关系”中提到的,线程读取共享数据到“工作内存中”,再从“工作内存”读取数据

所以此时在 t 线程中,参与 while 循环条件判断的 flag ,实际上是一个存储“工作内存”的 flag 副本。

当 flag 通过另一线程改变值,改变的是“主内存”中的 flag,t 线程并不能察觉。

因此 t 线程无法从 while 循环中跳出并结束

这就是内存可见性影响线程安全的情况之一。


二、指令排序

1)什么指令排序

指令排序是指编译自动调整原有代码的执行顺序,在保证逻辑不变的前提下,提高程序运行效率

指令排序也是编译优化方式之一。

2)指令排序存在什么问题

指令重排序的前提是“保证逻辑不变”。这一点在单线程环境下较容易判断,但是在多线程环境下,代码复杂程度高,编译器在编译阶段代码执行效果的判断存在困难。

因此在多线程环境下,代码重排序很容易导致优化后和优化前的逻辑不等价。

图示演示指令重排序可能出现的问题


三、关键字 volatile 

1)volatile作用什么
<1>

保证内存可见性。volatile 修饰变量每次被访问都必须从内存中读取,每次被修改都必须存储到内存中。

<2> 禁止指令重排序。volatile 修饰变量读写操作相关指令不允许被重排序。
2)内存可见性和指令重排序都是编译优化,怎么好像都是负作用

在大部分场景下,编译优化都能非常优秀的提高程序的运行效率,只是在多线程编程部分关键代码中,存在线程不安全的风险

3)volatile 不保证原子
volatilesynchronized本质区别synchronized 保证原子性,而 volatile 保证的是内存可见性。
4)合理使用 volatile 关键字

编译优化就好像一场激烈的风暴,而程序员要做的就是掌控这场风暴,必要时让风暴停一停。

为此,Java 提供了 volatile 关键字程序使用。当使用 volatile 关键字时,强制读写内存,禁止指令重排序,程序运行速度变慢,但数据准确性提高,线程变得安全了。

代码演示 volatile 的使用效果,沿用上文“内存可见性演示”中的代码:

public class Volatile_Demo0 {
    //有一个共享变量flag,注意该变量已经被 volatile 修饰public volatile static int flag = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建一个线程,线程中当flag为0时,一直循环判断;
        Thread t = new Thread(()->{
            while (flag == 0){}
        });

        //启动线程;
        System.out.println("run开始");
        t.start();

        //让main线程休眠两秒后,将flag的值改为1;
        Thread.sleep(2000);
        flag = 1;

        //让main线程等待t线程结束;
        t.join();
        System.out.println("run结束");
    }
}

//运行结果run开始
run结束

与上文“内存可见性演示”中的代码唯一不同,就是在共享变量 flag 上,使用了 volatile 进行修饰。
但这次的结果程序正常执行完毕,证明了 volatile 的作用

原文地址:https://blog.csdn.net/zzy734437202/article/details/134757070

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_32596.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注