volatile变量线程安全吗?java内存模型与volatile关键字

java内存模型:是为了定义程序中多个线程间可共享变量(以下简称变量)的读写规则
java内存模型规定了所有变量都存储在主内存中,每条线程都有自己的工作内存,工作内存中保存了被其使用到的所有变量的主内存拷贝副本,所有操作都是对该副本直接进行,而不能直接读写主内存中的变量。当变量被某个线程修改后,虚拟机会将修改后的值同步回主内存。线程之间各自独立,对于同一份变量的copy是不能共享的。从定义上看,我们可以把主内存认为是java堆,工作内存是java栈,变量是堆中的对象实例数据。
三者关系图如下:

由此我们不难看出,如果一个变量可以被多个线程访问,而且这个变量或者其访问程序未经过特殊处理,那就很可能出现各个线程中变量值不一致的情况,比如有如下代码:

int s=1;

---------------------------线程A代码如下:

s=2;

---------------------------线程B代码如下:

if(s==2){

    doSomething...

}

由内存模型我们可以推断出即使在A执行了代码之后,线程B中的判断也不一定会成立,因为此时s的值可能还未被同步回主内存中,B中的s的值不是最新被修改后的值,也就是内存不可见。对此我们程序员自有妙招,那就是加锁同步,synchronized  lock都可以解决此类问题,保证变量的并发安全。但是synchronzied和lock是java中的重量级操作,java还提供了更轻量级的同步机制,那就是volatile,关于volatile和synchronzied的比较后面会有叙述

volatile变量具有内存可见性以及防止重排序量大重要特征:
可见性:当一个线程修改了volatile变量的值后,这个新的值对其他线程都是可以立即得知的。上面的代码中,如果将s设置为volatile变量,当A修改了s后,虚拟机会立即将线程A工作内存中的该值copy并写到到主内存中的s中,并且强制其他线程无效化其工作内存中对s变量的Cache,然后将其刷新为最新的值,是为可见性。注意:volatile只能保证变量的内存可见性,而并不代表用volatile修饰的变量可以保证线程安全,只有当对volatile变量的操作都是原子性操作时,才可以保证该变量是线程安全的,上面代码里的=号就是原子性操作。
   而如果将s=2这一句换成s++;那情况就完全不一样了,不同于=号的赋值操作,s++是一个复合操作,这句代码会被翻译为 读取s的值,将值加1,将+1后的结果写入s 这三步操作,多线程情况下,这种复合操作是谈不上线程安全的... volatile 可见性的适用场景基本就只有下面代码表述的类似的情况
volatile boolean shutdownRequested;
        public void shutdown(){
            shutdownRequested=true;
        }
        public void doWork(){
            while(!shutdownRequested){
            //do stuff
        }
         防止重排序:从硬件架构上讲,指令重排序是指CPU采用了允许将多条指令 不按程序规定的顺序分开发送给各相应电路单元处理。在虚拟机层面上讲,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。
也就导致了实际代码执行的顺序并不一定是按照我们所看到或者想到的代码顺序来执行的,但并不是说指令任意重排,CPU需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。
看一个例子:
Map configOptions;
char[]configText;
//此变量必须定义为volatile
volatile boolean initialized=false;
-------------------------------------------------------------
//假设以下代码在线程A中执行
//模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true;
-------------------------------------------------------------
//假设以下代码在线程B中执行
//等待initialized为true,代表线程A已经把配置信息初始化完成
while(!initialized){
sleep();
}
//使用线程A中初始化好的配置信息
doSomethingWithConfig();
如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中最后一句的代码“initialized=true”被提前执行(在配置信息被读取之前被赋值为true),这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生。我的博文"单例模式中的那些坑"中通过介绍双重检查单例模式引出了重排序,感兴趣的可以去看一下...

volatile的同步机制的性能确实要优于锁( 使用synchronized关键字或java.util.concurrent包里面的锁) , 但是由于虚拟机对锁实行的许多消除和优化, 使得我们很难量化地认为volatile就会比synchronized快多少。 如果让volatile自己与自己比较, 那可以确定一个原则: volatile变量读操作的性能消耗与普通变量几乎没有什么差别, 但是写操作则可能会慢一些, 因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。 不过即便如此, 大多数场景下volatile的总开销仍然要比锁低, 我们在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求。

猜你喜欢

转载自blog.csdn.net/wb_snail/article/details/80727353
今日推荐