Volatile
一、Volatile介绍
Volatile 是Java虚拟机提供的轻量级的同步机制
特性:
- 保证可见性
- 不保证原子性
- 保证有序性
我们将围绕以上三点来探讨Volatile的作用
1.1、验证可见性
案例:启动一个线程,并使其死循环状态,通过额外线程修改共享变量使其关闭
- 注意必须是共享变量(JMM特性)
package com.migu.jmm;
import java.util.concurrent.TimeUnit;
/**
* 测试共享变量可见性
*/
public class Test {
// private static boolean f = true; 未使用volatile修饰,会死循环
private volatile static boolean f = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (f){}
}).start();
TimeUnit.SECONDS.sleep(3);
f = false;
}
}
结果:由于Volatile保证可见性,则使其线程正常关闭
1.2、测试不保证原子性
一、非原子操作
案例:通过多个线程对共享变量自增,看看是否自增过程中,由于由于多个线程并行执行,是否会由于原子性而保证结果正常
num++
,并不是一个原子性操作- 通过对其反汇编【javac -p】,可发现一共分三步
- 获取值、修改值、写回值
- 通过对其反汇编【javac -p】,可发现一共分三步
package com.migu.jmm;
/**
* 测试Volatile不保证原子性(取自狂神例子)
*/
public class VolatileAtomicity {
private static volatile int num = 0;
private static void add(){
num++;
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 每个线程对其自增1000次,一共10000次
}
}).start();
}
// 当其他线程未执行完毕,main线程则对其让步,每次只要进入运行态就会转到就绪态
// GC守护线程不一定会执行
while (Thread.activeCount() > 1) Thread.yield();
System.out.println(num);
}
}
输出:有9587、9352、7382.。。。。。。。
看来Volatile
只保证了可见性,在某些情况下,由于跑得慢的线程,最后执行,然后此时num
的值还是很小,由于可见性原因导致其他线程,num
值也跟着变小,结果就变以上那样了
二、原子操作
有一种办法就是对其加锁,我们只需在以上案例中对add
方法加锁即可,但不推荐,耗性能
还有一种方案使用原子类
- 原子类位于
java.util.concurrent.atomic
包下:是一种支持单变量无锁线程安全编程的类的工具包
对以上案例进行改进:
package com.migu.jmm;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 测试Volatile不保证原子性(取自狂神例子)
*/
public class VolatileAtomicity {
private static volatile AtomicInteger atomicInteger = new AtomicInteger();
private static void add(){
atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 每个线程对其自增1000次,一共10000次
}
}).start();
}
// 当其他线程未执行完毕,main线程则对其让步,每次只要进入运行态就会转到就绪态
// GC守护线程不一定会执行,这边大于1即可
while (Thread.activeCount() > 1) Thread.yield();
System.out.println(atomicInteger.get());
}
}
输出:10000
为什么能出现这样的效果,是由于CAS,通过unsafe
类直接在内存修改,具体讲到锁时在阐述
1.3、指令重排序
一、指令重排介绍
指令重排是一种优化。
编译器可能会进行重排、并行指令也可能会重排、内存系统也可能会进行重排,但是重排是有条件的
不能违背happen-before原则,即数据之间的依赖性。
来瞅瞅这个例子:
int x = 1;
int y = 2;
x = x + 1;
y = x + y;
其中第三条和第四条指令的执行需要依赖于第一条和第二条
也就是重排序的结果不可能为: 4–>3–>2–>1这种情况
但是在多线程环境下有可能会发生某些错误
二、产生依赖的三种情况
以下三种情况不会发生指令重排【针对同一个变量】
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量之后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量之后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量之后,再写这个变量。 |
三、多线程下的指令重排
以下案例验证多线程环境下发生指令重排时,会出现什么情况
通过多次的重复读写,判断是否存在指令重排的错误
- 下面案例中
a
变量和flag
变量并无直接关系(则可测试)
package com.migu;
public class Test {
private int a = 0;
private boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
for (int i = 0; i < 500000; i++) {
// 两块线程启动进行测试
new Thread(() -> {
test.write();
}).start();
new Thread(() -> {
test.read();
}).start();
// 这块循环只是为了初始化,具体无关
while (true) {
if (Thread.activeCount() == 1) {
// 每进行一次写读,则对其初始化
test.a = 0;
test.flag = false;
break;
}
}
}
}
// 看看是否会先执行flag=true,再去执行a=1
public void write() {
a = 1;
flag = true;
}
public void read() {
if (flag && a == 0) {
System.out.println(Thread.currentThread().getName() + "----" + a + "-------");
}
}
}
输出结果:(在某次读写过程中,线程A的write
只执行了flag = true
,便被线程B的read
执行,而导致出现以下的场景)
结论:多线程环境下的指令重排序会导致结果异常
解决方案:为其中任何一个变量,加一个volatile
关键字
四、Volatile原理
原因是通过Volatile修饰会为该变量的操作指令中的前后都加一个内存屏障,防止重排序,那么那些优化机制就不会对其重排序(更多细节就不懂了)
五、注意点
使用synchronized可以代替volatile关键字吗?
// synchronized同步块之间虽然是原子性操作,但是里面是非原子操作
// 所可以是可能发生指令重排的
// Synchronized虽然不能防止指令重排
// 但是至少能保证最后结果是不会出错的
六、应用场景
单例模式:双重检验懒汉式
- 注意创建一个对象并不是一个原子操作,所以在这段创建过程是会存在指令排序,且非原子操作,所以执行过程是存在被其他线程读取并从中修改的情况
- 1、分配内存空间
- 2、执行构造方法,创建并初始化对象
- 3、执行引用
在单线程情况下上面步骤是不会产生影响
package com.migu.jmm;
public class Singleton {
public static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
单例模式更多优化可查看专门博客教学(比如什么防反射之类的)