volatile语义
一.保证此变量对所有的线程的可见性,
二.禁止指令重排序优化。
①保证可见性
这里的“可见性”,是指,当一个线程修改了这个变量的值,新值对于其他线程来说是立即得知的。volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
②保证顺序性
volatile关键字对顺序性的保证比较霸道的一点,直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但是对于volatile前后无依赖的指令可以随便排序。
int x = 0;
int y = 1;
volatile int z = 20;
x++;
y--;
在语句volatile int z = 20之前,先执行x的定义还是先执行y的定义,并不关心,只要能百分百保证在执行到z=20的时候,x=0,y=1,同理关于x的自增以及y的自减操作都必须在z=20以后才能发生。
③volatile不保证原子性
volatile变量的运算在并发下一样不安全,是因为java里面的运算并非是原子操作。
//使用volatile修饰共享变量i
private static volatile int i = 0;
//闭锁,保证10个线程全部执行完毕
private static final CountDownLatch latch = new CountDownLatch(10);
private static void inc() {
i++;
}
public static void main(String[] args) {
for (int j = 0; j < 10; j++) {
new Thread(() -> {
for (int x = 0; x < 1000; x++) {
inc();
}
//使计数器-1
latch.countDown();
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
结果是个随机数.
i++的操作实际是由三步组成,
1)从主存中获取i的值,然后缓存至线程工作区中
2)在线程工作内存weii的值+1操作
3)将i的最新值写入主内存
volatile原理和实现机制
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
OpenJDK下,unsafe.cpp中,被volatile修饰的变量存在一个"lock;"的操作,(源码去openjdk下看)
"lock;"操作实际上相当于一个内存屏障,改内存屏障会为指令的操作执行提供一下几个保障:
- 确保指令重排序时不会将其后面的代码排到内存屏障前面
- 确保指令重排序时不会将其前面的代码排到内存屏障后面
- 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
- 强制将线程工作内存中值得修改刷新至主内存中
- 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效
volatile和Synchronized
- 使用上的区别
volatile关键字只能使用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等。
synchronized 关键字不能用于对变量的修饰,只能用于修饰方法或者语句块
volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null
- 对原子性的保证
volatile无法保证原子性
由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此能保证代码的原子性。
- 对可见性的保证
两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同。
synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中。
相比较于synchronized关键字volatile使用机器指令(偏硬件)"lock;"的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载。
- 对有序向的保证
volatile关键字禁止JVM编译器以及处理器对其进行重排序,所以它能够保证有序性。
虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况,比如:
synchronized(this){
int x = 10;
int y = 20;
x++;
y=y+1;
}
x和y谁最先定义进行运算,对程序来说没有任何的影响,另外x和y之间也没有依赖性,但是由于synchronized关键字同步的作用,在synchronized的作用域结束时x必定是11,y必定是21,也就是说达到最终的输出结果和代码编写顺序一致。
volatile 不适用情况
不符合一下两条的规则运算场景中:
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
推荐阅读: