1.内存可见性
volatile是Java提供的一种轻量级的同步机制,同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,倘若能恰当的合理的使用volatile,自然是美事一桩。
Java内存模型
为什么出现这种情况呢,我们需要先了解一下JMM(java内存模型)
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,**JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。**这三者之间的交互关系如下
大概了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,线程A将对变量X发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B缓存了变量X的初始值,此时可能没有观测到变量X的值被修改了,就会导致线程B中的变量X还是一开始的初始值。那么这种共享变量在多线程模型中的不可见性如何解决呢?比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,有点炮打蚊子的意思。比较合理的方式其实就是volatile
volatile具备两种特性,第一就是保证共享变量对所有线程的可见性。将一个共享变量声明为volatile后,会有以下效应:
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操作会导致其他线程中的缓存无效。
2.注意复合操作和原子性
num++不是个原子性的操作,而是个复合操作。我们可以简单讲这个操作理解为由这三步组成:
1.读取
2.加一
3.赋值
所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中,最终导致了num的结果不合预期,而是小于30000
可以使用java并发包中的原子操作类原子操作类是通过循环CAS的方式来保证其原子性的
3.禁止指令重排序优化
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
package com.youyou;
/**
* Created by youyou on 2019-11-25.
*/
public class ChongPaiXuVolatile {
static int a=0;
static boolean status=false;
public static void change(){
a=1;//1
status=true;//2
}
public static void print(){
if (status){//3
System.out.println(a);//4
}
}
public static void main(String []args) {
new Thread(new Runnable2()).start();
new Thread(new Runnable2()).start();
new Thread(new Runnable1()).start();
new Thread(new Runnable2()).start();
new Thread(new Runnable2()).start();
new Thread(new Runnable2()).start();
new Thread(new Runnable2()).start();
}
static class Runnable1 implements Runnable{
@Override
public void run() {
change();
print();
}
}
static class Runnable2 implements Runnable{
@Override
public void run() {
print();
}
}
}
假设在Runnable1执行完成后change方法的status=true;的方法后,其他Runnable2的a一定会是1吗?答案是不确定的,因为,为了提供程序并行度,编译器和处理器可能会对指令进行重排序,而上例中的步骤1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=1。而此时Runnable2会顺利到达4处,而线程Runnable1中a=1这个操作还未被执行,所以a的结果也有可能依然等于0。