背景:
volatile变量在多线程环境中经常使用,我们利用其线程可见性和禁止指令重排的特性,实现比synchronized更轻量级的变量同步共享;
虽然我们经常使用或看见volatile关键字,但是 很多人却不一定测试过加volatile和不加到底有什么区别! 在此我们测试经典应用场景下volatile关键字具体会产生什么影响!
环境:win10 - jdk1.8
测试过程:
1.前言
Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的!
关键字volatile用来修饰变量,可以简单理解为告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对该变量的可见性。
状态标志
(通常为boolean类型) 是volatile变量最经典的应用之一,我们通过这个用法来测试!
注:有很多博客也是如此测试的,但大多写的比较粗略,难以测试成功
2.普通变量测试
以下代码是通过两个线程实现测试,一个线程不停写status变量(未加volatile关键字),一个线程不停读status变量;我写了一些注释,大家应该看懂这段代码到底在干什么。
/**
* @Author: yuanj
* @Date: 2018/12/13 9:33
*/
public class TestWithoutVolatile {
//测试volatile效果的boolean状态标志变量(这种状态量也是volatile经典应用之一)
static boolean status;
static int i;
public static void main(String[] args) throws Exception {
new Thread(new WaitExitRunnable()).start();
//此处必须让WaitExitRunnable线程先运行一段时间才能测试成功!
//个人推测原因是WaitExitRunnable线程运行一段时间后才会将共享内存中的status缓存到本地内存,以后读都从自己本地内存读
Thread.sleep(1000);
new Thread(new SwapRunnable()).start();
}
/**
* 测试volatile变量影响的关键线程,无限读取并比较status和!status的值,根据程序是否退出来判断volatile关键字的影响
*/
static class WaitExitRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(i++);
// if条件里是非原子型判断操作;如果status值不变,会无限循环,过不去判断条件;但有SwapRunnable线程无限改变status值,所以推测可能存在这种情况:
// 读status和读!status之间status发生一次变化,这样判断条件就能通过,程序退出!
if (status == !status) {
System.out.println("exit");
System.exit(0);
}
}
}
}
/**
* 无限交换status和!status的值,看WaitExitRunnable线程能否观察到影响
*/
static class SwapRunnable implements Runnable {
@Override
public void run() {
//无限修改status的值
while (true) {
status = !status;
}
}
}
}
如果没有线程本地内存和共享内存的区别,大家把status变量当成是两个线程共享的变量,并且SwapRunnable线程对status的改变能立刻被WaitExitRunnable线程读到正确结果,那么该程序无论运行多少次都会在一定时间后停止!因为WaitExitRunnable线程的if条件是非原子性操作,总会碰到status == !status
的时候,导致程序退出!
但测试的结果是,每运行几次该代码都会出现一次死循环!程序无法结束!(我测试的时候一般是运行6次左右会出现一次死循环)
这说明此时SwapRunnable线程对status的写已经无法被WaitExitRunnable线程正确地读到了,变得不可见了;原因就是线程从本地内存读写,而没有同步读写到共享内存!
3.volatile变量测试
我们将status变量加上volatile关键字,其他不变,再次运行程序:
/**
* @Author: yuanj
* @Date: 2018/12/13 9:33
*/
public class TestVolatile {
static volatile boolean status; //加上volatile
static int i;
public static void main(String[] args) throws Exception {
new Thread(new WaitExitRunnable()).start();
Thread.sleep(1000);
new Thread(new SwapRunnable()).start();
}
static class WaitExitRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(i++);
if (status == !status) {
System.out.println("exit");
System.exit(0);
}
}
}
}
static class SwapRunnable implements Runnable {
@Override
public void run() {
while (true) {
status = !status;
}
}
}
}
大家测试可以发现,加上volatile后,无论该代码运行多少次,都没有再出现过死循环了!(我试了30多次吧,都没有出现死循环了,大家可以试下…)
通过这个对比测试,我们终于能相对直观地观察到volatile的威力了~