|
volatile定义 Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。 volatile的作用 先让我们说说volatile关键字的作用。它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。 volatile代码示例 单例模式(重排序) public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有
*/
private Singleton(){}
/**
* 单例实现
* @author fuyuwei
* 2017年5月14日 上午10:07:07
* @return
*/
public static Singleton getInstance(){
if(singleton == null){
synchronized (singleton) {
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
} |
我们知道实例化一个对象经过分配内存空间、初始化对象、将内存空间的地址赋值给对应的引用,上面的单例模式可以解释为分配内存空间、将内存地址赋值给对应的应用、初始化对象。上面的代码如果我们不加volatile在并发环境下可能会出现Singleton的多次实例化,假如线程A进入getInstance方法,发现singleton==null,然后加锁通过new Singleton进行实例化,然后释放锁,我们知道new Singleton在JVM中其实是分为3步,假如线程啊在释放锁之后还没来得及通知其他线程,这时候线程B进入getInstance的时候发现singleton==null便会再次实例化。 可见性 一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题
public class Volatile {
int m = 0;
int n = 1;
public void set(){
m = 6;
n = m;
}
public void print(){
System. out.println( "m:"+m+ ",n:"+n);
}
public static void main(String[] args) {
while( true){
final Volatile v = new Volatile();
new Thread( new Runnable(){
@Override
public void run() {
try {
Thread.sleep( 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
v. set();
}
}).start();
new Thread( new Runnable(){
@Override
public void run() {
try {
Thread.sleep( 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
v.print();
}
}).start();
}
}
} |
正常情况下m=0,n=1;m=6,n=6,通过运行我们发现了m=0,n6(需要长时间运行)
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :6
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :0, n :1
m :0, n :1
m :6, n :6
m :0, n :1
m :0, n :1
m :0, n :1
m :6, n :6
m :6, n :6
m :6, n :6
m :0, n :1
m :6, n :6
m :6, n :6 |
对volatile变量的写操作与普通变量的主要区别有两点:
(1)修改volatile变量时会强制将修改后的值刷新的主内存中。
(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。 volatile并不能保证原理性 package com.swk.thread;
public class Volatile {
private volatile int m = 0;
public void incr(){
m++;
}
public static void main(String[] args) {
final Volatile v = new Volatile();
for( int i= 0;i< 1000;i++){
new Thread( new Runnable(){
@Override
public void run() {
try {
Thread.sleep( 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
v.incr();;
}
}).start();
}
try {
Thread.sleep( 10000); // 确保1000次循环执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(v.m);
}
} |
输出结果:950,并不是我们想象中的1000,如果我们在incr加上synchronized,输出结果是1000
原因也很简单,i++其实是一个复合操作,包括三步骤:
(1)读取i的值。
(2)对i加1。
(3)将i的值写回内存。
volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。 volatile底层实现 在了解volatile实现原理之前,我们先来看下与其实现原理相关的CPU术语与说明
术语 | 英文单词 | 术语描述 |
---|
内存屏障 | memory barries | 是一组处理器指令,用于实现对内存操作的顺序限制 | 缓冲行 | cache line | 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存周期 | 原子操作 | atomic operations | 不可中断的一个或一些列操作 | 缓冲行填充 | cache line fill | 当处理其识别到内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存 | 缓存命中 | cache hit | 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从高速缓存中读取,而不是从内存中读取 | 写命中 | write hit | 当处理器操作数写回到一个内存缓存区域时,他首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是会写到内存 | 写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域 |
volatile是如何来保证可见性的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做什么事情。Java代码如下:
private valatile Singleton instance = new Singleton(); |
转成汇编代码如下
0x01a3de1d: movb $0× 0, 0× 1104800( %esi); 0x01a3de24: lock addl $0× 0,( %esp); |
当有volatile变量修饰时会出现lock addl $0×0,(%esp),Lock前缀的指令在多核处理器下会引发了两件事情
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。 volatile使用场景 一个线程写,多个线程读 volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
} |
结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁” volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。
public class CheesyCounter {
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
} |
----------------------------
原文链接:https://blog.csdn.net/fuyuwei2015/article/details/71939591
程序猿的技术大观园:www.javathinker.net
[这个贴子最后由 flybird 在 2020-02-24 12:50:47 重新编辑]
|
|