在看volatile关键字之前,先了解一个概念
什么是内存屏障?
内存屏障(Memory Barrier)也称为内存栅栏或栅栏指令,是一种CPU屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。内存屏障共分为四种类型:LoadLoad屏障:抽象场景:Load1; LoadLoad; Load2Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。StoreStore屏障:抽象场景:Store1; StoreStore; Store2Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见LoadStore屏障:抽象场景:Load1; LoadStore; Store2在Store2被写入前,保证Load1要读取的数据被读取完毕。StoreLoad屏障:抽象场景:Store1; StoreLoad; Load2在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
在一个变量被volatile修饰后,JVM会为我们做两件事:
1.在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。 2.在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。应用:
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。
这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。
这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Code:
public class Counter { public static int count = 0; public static void inc() { //这里延迟1毫秒,使得结果明显 try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //同时启动1000个线程,去进行i++计算,看看实际结果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //这里每次运行的值都有可能不同,可能为1000 System.out.println("运行结果:Counter.count=" + Counter.count); }}
运行结果:Counter.count=980
添加Volatile
public class Counter { public volatile static int count = 0; public static void inc() { //这里延迟1毫秒,使得结果明显 try { Thread.sleep(1); } catch (InterruptedException e) { } count++; } public static void main(String[] args) { //同时启动1000个线程,去进行i++计算,看看实际结果 for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { Counter.inc(); } }).start(); } //这里每次运行的值都有可能不同,可能为1000 System.out.println("运行结果:Counter.count=" + Counter.count); }}
Result:
运行结果:Counter.count=801
运行结果还是没有我们期望的1000,下面我们分析一下原因
每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存
变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
Volatile和Staticstatic指的是类的静态成员,实例间共享volatile跟Java的内存模型有关,线程执行时会将变量从主内存加载到线程工作内存,建立一个副本,在某个时刻写回。valatile指的每次都读取主内存的值,有更新则立即写回主内存。理解了这两点,逐句再来解释你的困惑:“既然static保证了唯一性”:static保证唯一性,指的是static修饰的静态成员变量是唯一的,多个实例共享这唯一一个成员。“那么他对多个线程来说都是可见的啊”:这里,static其实跟线程没太大关系,应该说对多个对象实例是可见的。你说对多个线程可见,虽然没什么毛病,因为静态变量全局可见嘛,但是把这个理解转到线程的上线文中是困惑的起因。“volatile保证了线程之间的可见性”:因为线程看到volatile变量会去读取主内存最新的值,而不是自个一直在那跟内部的变量副本玩,所以保证了valatile变量在各个线程间的可见性。“那么修改的时候只要是原子操作,那么就会保证它的唯一性了吧”:此时你说的“唯一性”,指的是各个线程都能读取到唯一的最新的主内存变量,消除了线程工作内存加载变量副本可能带来的线程之间的“不唯一性”。这里“唯一性”的含义跟第一句说的“唯一性”是不一样的。“这两个在我理解上我觉得差不多。”:其实解决问题的“场景”是完全不一样的。造成理解困惑最大的原因在于,这两个场景略有类似,以致混淆了:场景1:各个类的实例共享唯一一个类静态变量场景2:各个线程共同读取唯一的最新的主内存变量的值
synchronized开销比volatile大,volatile能够胜任的用volatile。
volatile不保证原子操作,所以,很容易读到脏数据
总结:
1.使用volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
在修改变量值以后直接写入到主内存中,并设置该变量在其他线程内存中的缓存行无效,其他线程再次使用该变量就会从主内存中读取(反应到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效)
2.并且阻止了指令重排,这些都是通过内存屏障实现的。
指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。指令重排是一把双刃剑,虽然优化了程序的执行效率,但是在某些情况下,会影响到多线程的执行结果。 volatile解决了long,double类型数据结构8字节赋值问题(仅在32位jvm下,如果是64位,那么就都是原子性的)
java对long和double的赋值操作是非原子操作!!long和double占用的字节数都是8,也就是64bits。
在32位操作系统上对64位的数据的读写要分两步完成,每一步取32位数据。这样对double和long的赋值操作就会有问题:如果有两个线程同时写一个变量内存,一个进程写低32位,而另一个写高32位,这样将导致获取的64位数据是失效的数据。 因此需要使用volatile关键字来防止此类现象。volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的。long,double在多线程中的情况:
java的内存模型只保证了基本变量的读取操作和写入操作都必须是原子操作的,但是对于64位存储的long和double类型来说,JVM读操作和写操作是分开的,分解为2个32位的操作,
这样当多个线程读取一个非volatile得long变量时,可能出现读取这个变量一个值的高32位和另一个值的低32位,从而导致数据的错乱。要在线程间共享long与double字段必须在synchronized中操作或是声明为volatile。
这里使用volatile,保证了long,double的可见性,那么原子性呢?
其实volatile也保证变量的读取和写入操作都是原子操作,注意这里提到的原子性只是针对变量的读取和写入,并不包括对变量的复杂操作,比如i++就无法使用volatile来确保这个操作是原子操作。