volatile关键字作用

volatile关键字作用一、作用简述内存可见性:保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。 屏蔽JVM指令重排序(防止JVM编译源码生成class时使用重排序)…

大家好,又见面了,我是你们的朋友全栈君。

一、作用简述

  1. 内存可见性:保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
  2. 屏蔽JVM指令重排序(防止JVM编译源码生成class时使用重排序):指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。

二、深入讲解

在Java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉。

Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块(synchronized) 和 volatile 关键字机制。

1、synchronized(不做过多解释)

同步块大家都比较熟悉,通过 synchronized 关键字来实现,所有加上synchronized 和 块语句,在多线程访问的时候,同一时刻只能有一个线程能够执行synchronized 修饰的方法 或者 代码块。

2、volatile

2.1 内存可见性详解

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的值。volatile很容易被误用,用来进行原子性操作。如果要深入了解volatile关键字的作用,就必须先来了解一下JVM在运行时候的内存分配过程:

在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改该线程栈中的副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这写交互:

volatile关键字作用

那么在了解完JVM在运行时候的内存分配过程以后,我们开始真正深入的讨论volatile的具体作用

请看代码:

public class VolatileTest extends Thread {
    
    boolean flag = false;
    int i = 0;
    
    public void run() {
        while (!flag) {
            i++;
        }
    }
    
    public static void main(String[] args) throws Exception {
        VolatileTest vt = new VolatileTest();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
        System.out.println("stope" + vt.i);
    }
}

上面的代码是通过标记flag来控制VolatileTest线程while循环退出的例子!

下面让我用伪代码来描述一下我们的程序

  • 首先创建 VolatileTest vt = new VolatileTest();
  • 然后启动线程 vt.start();
  • 暂停主线程2秒(Main) Thread.sleep(2000);
  • 这时的vt线程已经开始执行,进行i++;
  • 主线程暂停2秒结束以后将 vt.flag = true;
  • 打印语句 System.out.println(“stope” + vt.i); 在此同时由于vt.flag被设置为true,所以vt线程在进行下一次while判断 while (!flag) 返回假 结束循环 vt线程方法结束退出!
  • 主线程结束

执行预期:2秒钟以后控制台打印stope-202753974。之后由于变量被设置成true,导致线程退出->进程退出

实际执行结果:可是奇怪的事情发生了 程序并没有退出。vt线程仍然在运行,也就是说我们在主线程设置的 vt.flag = true;没有起作用。

在这里我需要说明一下,有的同学可能在测试上面代码的时候程序可以正常退出。那是因为你的JVM没有优化造成的!在DOC下面输入 java -version 查看 如果显示Java HotSpot(TM) … Server 则JVM会进行优化。

如果显示Java HotSpot(TM) … Client 为客户端模式,需要设置成Server模式  设置方法问Google

问题出现了,为什么我在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false?

 那么按照我们上面所讲的 “JVM在运行时候的内存分配过程” 就很好解释上面的问题了。

 首先 vt线程在运行的时候会把 变量 flag 与 i (代码3,4行)从“主内存”  拷贝到 线程栈内存(上图的线程工作内存)

 然后 vt线程开始执行while循环 

while (!flag) {
   i++;
}

while (!flag)进行判断的flag 是在线程工作内存(线程的栈内存)当中获取,而不是从 “主内存”(堆内存)中获取。

i++; 将线程内存中的i++; 加完以后将结果写回至 “主内存”,如此重复。整个过程只完成了将修改后的i值写回主存,而获取时没有重新重主存拿,还是从栈内存拿的(一直读的都是首次从主存load到线程工作内存的值false)

然后再说说主线程的执行过程。 我只说明关键的地方 

vt.flag = true;

主线程将vt.flag的值同样 从主内存中拷贝到自己的线程工作内存 然后修改flag=true. 然后再将新值回到主内存。

这就解释了为什么在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false。那就是因为vt线程每次判断flag标记的时候是从它自己的“工作内存中”取值,而并非再次从主内存中取值!

这也是JVM为了提供性能而做的优化。那我们如何能让vt线程每次判断flag的时候都强制它去主内存中取值呢。这就是volatile关键字的作用。

再次修改我们的代码:

public class VolatileTest extends Thread {
    
    volatile boolean flag = false;
    int i = 0;
    
    public void run() {
        while (!flag) {
            i++;
        }
    }
    
    public static void main(String[] args) throws Exception {
        VolatileTest vt = new VolatileTest();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
        System.out.println("stope" + vt.i);
    }
}

在flag前面加上volatile关键字,强制所有工作线程每次读取该值的时候都去“主内存”中取值在试试我们的程序吧,已经正常退出了。

2.2 防止指令重排序(防止JVM编译源码生成class时使用重排序)

编译期重排序的典型就是通过调整指令顺序,做到在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。
比如我们有如下代码:

int x = 10;
int y = 9;
x = x+10;

假设编译器直接对上面代码进行编译,不进行重排序的话,我们简单分析一下执行这段代码的过程,首先加载x变量的内存地址到地址寄存器,然后会加载10到数据寄存器,然后CPU通过mov指令把10写入到地址寄存器中指定的内存地址中。然后加载y变量的内存地址到地址寄存器,加载9到数据寄存器,把9写入到内存地址中。进行第三行执行时,我们发现CPU需要重新加载x的内存地址和数据到寄存器,但如果我把第三行和第二行换一下顺序,那么执行过程中对于寄存器的存取就可以少很多次,同时对于程序结果没有任何影响。

另一个例子可以看下面的双重检查锁构造单例的代码:

public class Singleton {
    private volatile static Singleton singleton;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (singleton == null) { // 1
            sychronized(Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(); // 2
                }
            }
        }
        return singleton;
    }
} 

实际上当程序执行到2处的时候,如果我们没有使用volatile关键字修饰变量singleton,就可能会造成错误。这是因为使用new关键字初始化一个对象的过程并不是一个原子的操作,它分成下面三个步骤进行:

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将 singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)

如果虚拟机存在指令重排序优化,则步骤2和3的顺序是无法确定的。如果A线程率先进入同步代码块并先执行了3而没有执行2,此时因为singleton已经非null。这时候线程B到了1处,判断singleton非null并将其返回使用,因为此时Singleton实际上还未初始化,自然就会出错。

虽然singleton使用synchronized进行了修饰,但是sychronized可以解决内存可见性,但是不能解决重排序问题。

但是特别注意在jdk 1.5以前的版本使用了volatile的双检锁还是有问题的。其原因是Java 5以前的JMM(Java 内存模型)是存在缺陷的,即使将变量声明成volatile也不能完全避免重排序,主要是volatile变量前后的代码仍然存在重排序问题。这个volatile屏蔽重排序的问题在jdk 1.5 (JSR-133)中才得以修复:这时候jdk对volatile增强了语义,对volatile对象都会加入读写的内存屏障,以此来保证可见性,这时候2-3就变成了代码序而不会被CPU重排,所以在这之后才可以放心使用volatile。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/138020.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • linux快捷键大全_电脑常用的快捷键大全

    linux快捷键大全_电脑常用的快捷键大全Linux常用快捷键,基础菜鸟推荐~

  • 1+X 云计算平台运维与开发认证(初级)样卷A——附答案

    传送门教育部:职业教育将启动“1+X”证书制度改革职业教育改革1+X证书制度试点启动1+X成绩/证书查询入口文章目录一、单选题(每题10分,共200分)二、多选题(每题15分,共300分)三、实操题(共500分)网络管理(70分)yum源管理(60分)数据库管理(70分)Linux存储LVM管理(60分)OpenStack管理(80分)Docker管理(80分)WordPress应用系…

  • mysql文件导入sqlserver_mysql导入sql文件命令

    mysql文件导入sqlserver_mysql导入sql文件命令问题来源有的时候,在使用MySQL数据库建表时,可能不需要直接在mysql数据库中建表,而需要导入外部已有的数据库表文件,方便我们使用。那么导入的方法呢?这里介绍一个很普遍也很简单的方法,步骤如下:导入步骤打开MySQL数据库,黑窗界面,如图:这里输入密码‘root’,回车。。。先确定你要建立的数据库名字,比如这里我新建数据库名字叫“house-01”,如下图。(说明:如果sql文件的内容中有创建数据库的语句,或者想将表存放在已有的数据库,在这里就不需要再创建数据库。即直接使用已经

  • java 中stopwatch,Stopwatch

    java 中stopwatch,Stopwatch/***Anobjectthataccuratelymeasureselapsedtime:themeasureddurationbetweentwo*successivereadingsof”now”inthesameprocess.*一个精确计算消耗时间的对象:计算在同一进程中两次连续读取当前时间之间持续的时间*Incontrast,wallti…

  • 误删数据?如何快速恢复Oracle数据库[通俗易懂]

    误删数据?如何快速恢复Oracle数据库[通俗易懂]误删oracle数据库中的数据,在不考虑全库备份和利用归档日志情况,怎样快速恢复数据呢?下面介绍3种方法。1.利用oracle提供的闪回方法进行数据恢复,适用于delete删除方式首先获取删除数据的时间点:select*fromv$sqlwheresql_textlike’%table_name%’;1根据结果中的sql_text内容,找到delete执行语句对应的删除时间点,执行下面语句查询出删除的数据。select*fromtable_nameasoftim

  • python压缩文件夹为zip_python 压缩文件

    python压缩文件夹为zip_python 压缩文件本文仅供学习交流使用,如侵立删!联系方式及demo下载见文末zipfile压缩文件夹defzipDir(self,dirpath,outFullName):”””压缩指定文件夹:paramdirpath:目标文件夹路径:paramoutFullName:压缩文件保存路径+xxxx.zip:return:无”””zip=zipf

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号