目录
什么是Volatile
Volatile是JVM提供的一种轻量级的同步机制(Synchronized是重量级) ,它用来确保将变量的更新操作通知到其他线程。
在访问volatile变量时不会执行加锁操作,因此也不会执行线程阻塞操作。因此volatile是轻量级的。
volatile的读性能消耗与普通变量相同,但写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不会发生乱序执行。
Volatile的特性
1.保证了可见性
2.不保证原子性
3.禁止指令重排
Volatile保证了可见性
我们拿上一篇博客举例,没有加同步机制的内存操作。子线程无法获取到主线程更改后的值
https://blog.csdn.net/Delicious_Life/article/details/106311118
我们使用Volatile关键字再试试,可以看到子线程在主线程醒来后,感知到变量的值变量,不再符合子线程的执行条件,执行完毕
即volatile变量num被子变量看见了
Volatile不保证原子性
下面的例子是要测试子线程在执行时,是否不能被打扰和分隔,即原子性。如果保证了原子性,应该打印的线程数为20000
public class Atomic {
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
//理论上num结果应该是20x1000=2万
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j=0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2) { //如果存活的线程大于2个,即除了main和gc还有其他线程
Thread.yield(); //执行线程礼让
}
System.out.println(Thread.currentThread().getName()+" " +num);
}
}
但很显然,无论测试多少次,main执行的线程都不到20000,也就是说出现了覆盖写的现象
如何解决线程执行的原子性呢?我们先来看看问题存在哪里?
当反编译这段代码时,可以看到num++这一操作分了3步执行,也就证明这一操作不是原子性的。
假设线程1执行num++,执行到了第二步+1还没写会主存时,时间片用尽。调用其他线程,当其他线程更新完主存后,线程1由于已经执行完获取值的操作而不会再次读取新num值,导致线程1算出的num值覆盖掉了线程2算出的num值~
我们现在就要考虑用什么来代替num++操作了。JUC为我们提供了一个atomic包
我们使用其中的AtomicInteger类,我们调用num的getAndIncrement方法执行+1操作。这个方法的基于CAS原理,它其实是调用操作系统的方法直接修改内存中的值,之后的博客会分析到
可以看到,这下保证了操作的原子性~
Volatile禁止了指令重排
什么是指令重排,就是计算机并不是安装你写程序的顺序去执行的。
指令重排时不同变量如果有依赖关系是不会进行重排的,下图中的执行顺序永远都不会是4123
但如果没有依赖关系就会进行指令重排
注意如果线程B先执行完毕,会把a的值更改后告诉了主存,主存告诉了线程A,线程A执行完毕后x=2了,而上个例子不进行指令重排的话,x=0.
Volatile如何禁止了指令重排呢?加了Volatile关键字会让这个变量前后都加一层内存屏障,内存屏障的意思是禁止上面指令和下面指令顺序进行交换
哪些地方用到了Volatile
1.单例模式
2.读写锁
3.JUC中的大量方法~