volatile特性
volatile具备并发三大特性当中的两种:
- 可见性
简单地说就是volatile变量修改后,所有线程都能立即实时地看到它的最新值。
- 有序性
有序性是指系统在进行代码优化时,不能把在volatile变量操作后面的语句放到其前面执行,也不能将volatile变量操作前面的语句放在其后执行。
那接下来我们先来看下volatile关键字是如何解决多线程可见性问题的。
volatile可见性
下面的两个例子演示了变量使用volatile和未使用volatile时,变量更新对多线程执行的影响。
//未使用了volatile
public class NonVolatileDemo {
public static boolean stop = false;//任务是否停止,普通变量
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
while (!stop) { //stop=false,不满足停止条件,继续执行
//do someting
}
System.out.println("stop=true,满足停止条件。" +
"停止时间:" + System.currentTimeMillis());
});
thread1.start();
Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
stop = true; //true
System.out.println("主线程设置停止标识 stop=true。" +
"设置时间:" + System.currentTimeMillis());
}
}
NonVolatileDemo中,停止标识stop未使用volatile关键字修饰,初始值为false。
- 创建子线程thread1并启动,在子线程thread1任务中使用停滞标识stop作为判断条件:
- 当不满足停止条件时,线程会一直运行;
- 当满足停止条件,终止任务。
- 稍后,我们在主线程中设置停止标识为true。
我们希望在设置stop=true之后,子线程能够获得到判断条件的变化而停下来
但是执行代码,结果如下图,我们可以看到在主线程设置stop=true后,子线程未及时感知到stop的变化,还在继续执行任务。
也就是子线程并没有知道stop的值改变的这件事情,why???来看看图解:
大家如果对于read/use等指令是干什么的不了解的也不用担心,这是JVM的原子性指令,跟volatile如何实现有序性的内存屏障相关,我们会在下一篇文章中详细讲解。
那通过上图大家知道了一个普通变量在多线程环境下的运行机制,所以为什么出现这个现象也就清楚了,那我们怎样能够解决这个问题呢?使用volatile修饰我们的变量,让它对于所有线程都具有可见性,来看下面的代码
//使用了volatile
public class VolatileDemo {
public static volatile boolean stop = false;//任务是否停止,volatile变量
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
while (!stop) { //stop=false,不满足停止条件,继续执行
//do someting
}
System.out.println("stop=true,满足停止条件。" +
"停止时间:" + System.currentTimeMillis());
});
thread1.start();
Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
stop = true; //true
System.out.println("主线程设置停止标识 stop=true。" +
"设置时间:" + System.currentTimeMillis());
}
}
在VolatileDemo中,停止标识stop使用volatile关键字修饰,初始值为false。其他代码和NonVolatileDemo完全一致。
结果出来了,NonVolatileDemo代码就存在可见性问题;而在VolatileDemo中通过使用volatile关键字,很简单的保证了多线程下共享变量的可见性。
好奇volatile怎么实现的吗?来来来一张图弄懂它!
现在是不是一目了然了呢?不过大家要注意一点,为了让大家好理解我引入了工作内存的概念,但是实际上工作内存不是真实存在的,想要了解真实的实现逻辑需要了解更多偏硬件的知识,大家有兴趣了解可以关注公众号,回复关键字"工作内存”来领取相关资料。
而且可见性的实现还有更深层的原理,需要配合有序性和内存屏障才能讲透彻,所以有兴趣的同学记得关注下一篇原理剖析哦
初步了解了volatile的基本使用及其可见性,下面我们来看看volatile和synchronized的区别
先上段代码,使用synchonized实现VolatileDemo功能,来直观感受下。
public class SychronizedDemo {
public static boolean stop = false;//任务是否停止
//同步静态方法,设置stop
public static synchronized void setStop(boolean flag) {
SychronizedDemo.stop = flag;
}
//同步静态方法,获取stop
public static synchronized boolean isStop() {
return stop;
}
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
while (!isStop()) { //stop=false,不满足停止条件,继续执行
//do someting
}
System.out.println("stop=true,满足停止条件。" +
"停止时间:" + System.currentTimeMillis());
});
thread1.start();
Thread.sleep(100);//保证主线程修改stop=true,在子线程启动后执行。
setStop(true); //true
System.out.println("主线程设置停止标识 stop=true。" +
"设置时间:" + System.currentTimeMillis());
}
}
在SychronizedDemo中,停止标识stop为普通静态变量,初始值为false。stop的设置方法setStop或获取方法isStop都为同步方法,可以保证锁对象SychronizedDemo类静态变量stop的可见性。执行代码,结果如下图,我们可以看到在主线程设置stop=true后,子线程同时感知到stop的变化终止了任务。
大家可以看到volatile和synchronized都实现了同样的功能,但底层实现是有一定差异的。
volatile关键字是Java提供的最轻量级的同步机制,为字段的访问提供了一种免锁机制,使用它不会引起线程的切换及调度。这时使用volatile要比synchronized要简单有效的多,如果使用synchronized还会影响系统的吞吐量。
那volatile关键字即可以保证可见性,而且使用起来这么方便,那它是解决可见性的万能药吗?我能用volatile代替synchonized吗?
事实上,volatile关键字并不是万能的,因为我们前文讲到了volatile并不能保证原子性。
volatile的正确使用方式
使用 volatile 变量的主要原因是单个字段同步操作的简易性。如果只使用了volatile就能实现线程安全,那就放心的使用它,如果同时还需要添加其他的同步措施,那就不要使用。
正确使用的场景举例:变量本身标识是一种状态,或者引用变量的某些属性状态,在代码中需要确保这些状态的可见性,这时就可使用volatile。volatile 变量仅仅是一个状态标识,用于指示发生了一个重要的一次性事件,例如完成初始化标识或请求终止标识。
volatile boolean stop=false;//volatile 变量,用于停止请求的状态标识
public void shutdown() {//停止请求
stop = true;
}
public void doWork() {
while (!stop) {//判断是否需要停止
// do Something
}
}
这样只要任何一个线程调用了shutdown(),其他线程在执行doWork时都可以立即感知到stop变量的变化,这时就可以大胆的使用volatile。这种类型的状态标记的一个公共特性是:通常只有一种状态转换,如标志从false 转换为true。