场景
volatile这个在多线程使用时能保证线程间的可见性。具体怎么用呢?举个例子:
public class VolatileVisibilityTest {
private static boolean initFlag = false;
//private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("waiting data");
while (!initFlag) {
}
System.out.println("========success!!!");
}).start();
Thread.sleep(2000);
new Thread(() -> prepareData()).start();
}
private static void prepareData() {
System.out.println("开始修改initFlag...");
initFlag = true;
System.out.println("initFlag修改成功...");
}
}
如上面的代码,结果是什么呢?会不会打印 “========success!!!”。我们来看结果
"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...
程序一直在whlie循环中,如果想要走出while循环,需要对initFlag 添加Volatile关键字修饰。
"C:\Program Files\Java\jdk1.8.0_212\bin\java.exe" ...waiting data开始修改initFlag...initFlag修改成功...========success!!!
如上结果,加了Volatile后会打印 “========success!!!”。
JMM线程模型
如上图,线程获取数据是先从主内存中获取,然后将数据拷贝到工作内存。如果A线程改变了共享变量,此时只是在工作内存中改变了数据。对于其他线程来说是不可见的。
那共享变量加了关键字后为什么会可见了呢?
在解读上面图之前我们先来解释下几个指令操作。
read(读取):从主内存读取数据load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内存读取数据来计算assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存write(写入):将store过去的变量值赋值给主内存中的变量lock(锁定):将主内存变量加锁,标识为线程独占状态unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
volatile保证可见性
如上图:线程1和线程2去主内存中拿(通过read指令)数据initFlag并将其放在(通过load指令)各自的工作内存中,此时initFlag的值为false。当线程2要修改initFlag的值为true,需要经历下面几步:
1)将工作内存中的initFlag 值加载(use指令)到线程2的执行引擎中
2)执行引擎将initFlag的值改为true
3) 将修改的值assign到工作内存中
4)将工作内存中的值store到主内存中
5)将主内存的变量赋值(通过write命令)
在第4步store时,数据会经过cpu总线,这时线程1会嗅探到值的变化。就会从主内存中获取新值。等等,这里可能你会有疑问,如果新值经过总线还没到达主内存中,这时线程1就去主内存中获取值,还是以前的旧值啊。那系统是怎么解决的呢。
当心智经过总线时,会上一把锁(lock),线程1是不能去主内存中获取值的,当执行了第5步,释放了锁(unlock),线程1才能去主内存中获取。
当线程1嗅探到值有变化,会让自己工作内存中的initFlag失效。然后线程1就执行下面步骤:
1)从主内存中read新值
2)将新值load进工作内存中
3)将工作内存中的变量赋新值
4)将新值use进线程1的执行引擎中
到这里变量initFlag就实现了可见性。
volatile不保证原子性
注意:volatile并不能保证原子性
public class VolatileAtomicTest {
public static volatile int num = 0;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
}
上面的输出会是10000么,实际输出为:9985。为什么会这样呢
原因是虽然num在线程间是可见的,但是数据从线程的工作内存同步到主内存是需要时间的,这时其他线程自己有自己的计算,导致结果不可预测。num++经历了两步:a = num + 1;num = a。这两步不能保证原子性。如果想保证原子性,需要对increase()方法加锁。
volatile静止指令重排
public class VolatileSerialTest {
static int x = 0, y = 0;
public static void main(String[] args) throws InterruptedException {
Set<String> resultSet = new HashSet<>();
Map<String, Integer> resultMap = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
x = 0;
y = 0;
resultMap.clear();
Thread one = new Thread(() -> {
int a = y;
x = 1;
resultMap.put("a", a);
});
Thread other = new Thread(() -> {
int b = x;
y = 1;
resultMap.put("b", b);
});
one.start();
other.start();
one.join();
other.join();
resultSet.add("a=" + resultMap.get("a") + "," + "b=" + resultMap.get("b"));
System.out.println(resultSet);
}
}
}
上面代码你觉得会输出什么呢?
答案是:
[a=0,b=1][a=1,b=0][a=1,b=1] [a=0,b=0] //不可思意吧
第四种输出不可思议吧,按照程序逻辑。是不可能出现[a=0,b=0]的情况的。其实CPU会对代码的执行顺序进行优化,及指令重排。
指令重排可以提高CPU处理速度。
如果给x,y加上volatile修饰,则不会出现指令重排,[a=0,b=0]就不会出现。
我们在编写懒汉式的单列模式时,也需要给对象加volatile修饰。
public class LazySimpleSingleton {
private static volatile LazySimpleSingleton instance = null; //需要volatile修饰
private LazySimpleSingleton(){
}
public static LazySimpleSingleton getInstance() {
if (instance == null) {
synchronized (LazySimpleSingleton.class) {
if (instance == null) {
instance = new LazySimpleSingleton();
}
}
}
return instance;
}
}
不加volatile,由于存在指令重排。线程A会出现b,c顺序颠倒情况。线程B进去获得实例可能未完全初始化。这时线程C执行方法getInstance(),返回的对象是未完全初始化的值。