可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
public class VolatileDemo { private static boolean flag = true; //main线程 public static void main(String[] args) { //线程处于死循环状态,由于线程之间的不见性该线程会一直执行 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { while (flag) { } }); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { executor.shutdown(); } flag = false; } }运行结果一直处于死循环状态
加上volatile关键字
public class VolatileDemo { private volatile static boolean flag = true; //main线程 public static void main(String[] args) { //线程处于死循环状态,由于线程之间的不见性该线程会一直执行 ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { while (flag) { } }); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { executor.shutdown(); } //停止死循环 flag = false; } }运行结果停了1s后就结束了
那么为什么volatile会保证线程间的可见性呢?
因为Java内存模型是将变量修改后,将其新值同步到主内存中,在变量读取前从主内存刷新变量值这种依赖主内存做媒介的方式来实现可见性的
普通变量以及volatile变量都有这样得,但是volatile的特殊性在于, 新值立即刷新同步到主内存,以及使用前也会立即从主内存中刷新
Demo
public class VolatileDemo02 { private static int num = 0; public static void add() { num++; } public static void main(String[] args) { for (int i = 0; i < 200; i++) { new Thread(()->{ for (int j = 0; j < 100; j++) { add(); } }).start(); } //等待所有累加线程都结束,再执行main线程后的输出语句 //为啥不是1而是2?因为IDEA会有一个Monitor-Ctrl-Break的线程 while (Thread.activeCount()>2) { //Thread.currentThread().getThreadGroup().list(); Thread.yield(); } //理应输出20000,结果却不一定 System.out.println(num); } }加上Volatile关键字
public class VolatileDemo02 { private volatile static int num = 0; public static void add() { num++; } public static void main(String[] args) { for (int i = 0; i < 200; i++) { new Thread(()->{ for (int j = 0; j < 100; j++) { add(); } }).start(); } //等待所有累加线程都结束,再执行main线程后的输出语句 //为啥不是1而是2?因为IDEA会有一个Monitor-Ctrl-Break的线程 while (Thread.activeCount()>2) { //Thread.currentThread().getThreadGroup().list(); Thread.yield(); } //理应输出2000 控制台却输出一直低于2000 System.out.println(num); } }运行结果
由此可见volatile不能保证原子性问题
那么遇到这种问题咋办?
加上synchronized,juc的锁或者把num的类型改成原子类型,直接操作内存,这三种方法都可以保证原子性
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。
出现这种问题的原因 : i++不是原子性的
解释下这个Demo为啥i++不是原子性的
由于方便我们就不使用JITwatch等工具来查看汇编分析**i++**发生了啥
感兴趣的可以分析一下
我们就使用javap -v 命令对class文件进行反编译得到字节码指令集
从这个有关add方法的指令可以看出从getstatic 把num的值取到栈顶,此时volatile关键字可以保证num的值是正确的,但是
但是执行到iconst_1,iadd指令的时候,其他的线程可能就以及把num的值给改变了,而操作栈顶的数据就是过期的数据,然后调用putstatic将过期的数据同步到主内存中
由于volatile种特性的存在总结其使用条件
使用条件 :
不与其它状态变量共同参与不变约束(比如volatile int a;while(a > b) 而b就是其它状态变量)对此变量的写操作不依赖与当前值(比如a = 1就是不依赖当前值,a = a + 1就依赖了当前值)实例
public class Main { private volatile boolean shutdown = false; public void do() { while (!shutdown) { // doSomething(); } } public void shutdown() { shutdown = true; } }什么是指令重排 : 就是你写的源代码再计算机执行顺序适用一些规则改变执行顺序,来增加cpu并行执行效率
源代码 -> 编译器优化的重排->指令并行也可能会重排->执行
int a = 1; //step1 int b = 2; //step2 a = a + 3; //step3 b = b + a; //step4 我们所期望的执行顺序: 1234;i 实际上顺序可能会发生改变如: 2134, 1324; //但是有个问题 可能的执行的顺序会是4132吗? 不可能!!!处理器再进行指令重排的时候,考虑 : 数据之间的依赖性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
那volatile为啥可以禁止指令重排呢?
我们可以通过JITwatch工具来查看volatile关键字到底再汇编底层到底干了些啥
public class VolatileDemo03 { private static volatile VolatileDemo03 instance; public static VolatileDemo03 getInstance(){ if (instance != null) { synchronized (VolatileDemo03.class) { if (instance == null) { instance = new VolatileDemo03(); } } } return instance; } public static void main(String[] args) { VolatileDemo03.getInstance(); } }这段代码是典型的DCL单例,其关键就是使用volatile
我们通过jitwatch插件获取到volatile关键字的汇编指令
或者使用 java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly classfilename Vm options 来反编译
但是这样生成的指令太多了根本找不到volatile , 所以需要使用JITwatch工具 从生成的汇编代码,可以看出Java里volatile的实现是用lock指令前缀来实现的:
0x00007f80950601b0: lock addl $0x0,(%rsp) ;*putfield sum ; - Test::add@12 (line 6) ; - Test::main@18 (line 16)这个汇编指令可以通过JITWatch这个应用来查看
下载 : https://github.com/AdoptOpenJDK/jitwatch
使用 : https://github.com/AdoptOpenJDK/jitwatch/wiki
参考博客 : https://blog.csdn.net/hengyunabc/article/details/26898657
Lock汇编指令:
将当前核中的对缓存的修改刷新到主内存中 会锁定当前缓存,将当前缓存中的数据回写到主存中。并通过缓存一致性协议保证操作是原子操作。 使其它核中的缓存无效 回写到主存中的操作会导致其它核中缓存的失效 此指令相当于一个内存屏障,保证不会将此指令之前的操作和此指令之后的操作和此指令进行重新排序。从这里可以看出volatile的汇编底层在赋值之后 (movb) 执行了一个lock add1 $0x0,(%rsp)指令这个指令相当于一个内存屏障
作用 : 把重排修改的顺序同步到主内存中 这样就意味着,之前所有重排操作都已经执行完成了