volatile
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度;
volatile原理是基于CPU内存屏障(Memory Barrier)指令实现的;
如果一个变量被volatile关键字修饰时:
- Java内存模型确保所有线程看到这个变量的值是一致的;
- 那么对这个变量的写是将本地内存中的拷贝刷新到共享内存中;
- 对这个变量的读会有一些不同,读是无视本地内存拷贝,直接从共享变量中去读取数据并拷贝到本地工作内存;
volatile并不能真正保证线程安全,它只能确保一个线程修改了共享数据后,其他线程能看到这个改动,即保证的是可见性,但不能保证原子性
内存可见性
由于Java内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存);线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中;
1.这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存;
2.当一个变量被volatile修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据;
3.volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中;
对象访问过程
当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量
相关操作指令
- read/load:从主存复制变量到当前工作内存
- use/assign:执行代码,改变共享变量值
- store/write:用工作内存数据刷新主内存相关内容
其中use and assign在线程执行过程中可以多次出现;
但是这一些操作并不是原子性,也就是说,在read and load之后,线程使用的变量值就是自己栈内存中的变量值备份副本了,这时如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
例如线程1,线程2 在进行read and load操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read and load操作,在后续的运算中,使用的均是自己栈内存中的副本,也就是使用5进行的运算,因此进行运算之后,更新主内存count的变量值也为6
这也就是导致两个线程及时用volatile关键字修改之后,仍会存在并发的情况的原因
总结:对于volatile修饰的变量,如果线程A在新值写入主内存前,线程B已执行了read/load指令(如果并发的情况下还没执行read/load指令则会从主内存同步最新值),那么在线程A将新值写入主内存后,线程B继续使用已加载的本地工作内存,这就导致了并发修改的问题
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
- 禁止进行指令重排序;
为什么要使用Volatile
- Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里;
- 线程之间的通信机制有两种:共享内存和消息传递;在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信;
说明:自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存;自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
使用volatile必须具备以下2个条件:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值;
- 该变量没有包含在具有其他变量的不变式中
volatile的局限性
只能保证内存可见性,不能用于构建原子的复合操作;当一个变量依赖其它的变量,或者当变量的新值依赖于旧值时,就不能是用volatile变量;