从JMM的角度浅析volatile关键字功能及作用以及其基础原理
看下这样一段代码:
我在一个方法中建立了个死循环,循环的判断条件为一个boolean类型的成员变量。
然后在main线程中,创建了一个新的名为 “t1” 的线程,去执行这个方法。
等待一秒后,mian线程自身,将该成员变量的值改为false,试图使其不满足条件从而循环终止。
按照正常的逻辑来说,按照脑海中预演的情况来说,应该是没问题的。
可是执行后却没有得到想要的结果,控制台输出 “start” 后再无反应,程序一直没有终止,即 死循环没有退出。
package com.skypyb.test; public class VolatileDemo { boolean flag = true; void run() { System.out.println("start"); while (flag) { //这是个死循环 } System.out.println("end"); } public static void main(String[] args) { VolatileDemo v = new VolatileDemo(); new Thread(v::run, "t1").start(); //等一秒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } v.flag = false; } }
想要让程序正确执行的话,将定义语句 boolean flag = true; 改为 volatile boolean flag = true; 程序就可以正确执行,这是为什么呢?
这里要涉及一点 java 内存模型,JMM。
JMM 里头,他有分配一块内存,叫主内存。类比于操作系统中的主存,主要存放线程间的共享数据(除去方法参数、局部变量)。
然后,每个线程在执行的过程当中,都有一个WorkingMemory,是主内存中部分数据的副本,只能该线程访问,类比于硬件的高速缓存。线程对数据的访问通常只能在workingMemory中进行,不能直接与主存交互。
比如cpu运行多个不同的线程的话,每个线程都有一个WorkingMemory,cup就是将主内存里边的数据读过来读到WorkingMemory里,然后再进行修改,比如+1+1+1+1,修改完后,再给他写回内存去。
我上边写的代码,这几个线程是怎么执行的呢?
在我的代码中,flag 是存在于堆内存中的 v 对象中。
当线程 t1 开始跑后,它首先会从主内存中把这个 flag 变量给它 copy 到自己的工作内存里边,然后开始运行。在cpu处理的过程之中,由于这cpu处理这个线程的部分他非常的忙,一直在while循环嘛,就不再去读主内存里边的数据了,一直在读自己的缓冲区(线程的工作内存)。
main 线程,改flag的参数,也是把 flag 参数 true 给读到自己的工作内存里边,然后进行修改,修改完事后,发现自己工作内存中的数据和主存中不一致,于是将自己工作内存中的数据给他刷新到主存中去。
这个时候就有问题了。第一个线程没有在主存里重新读啊?所以,这时候第一个线程就结束不了。
volatile 关键字功能及作用:
简单的来说,volatile 关键字,主要功能是使一个变量在多个线程间可见。
volatile关键字修饰的变量一旦在主存中被改变时,就会通知别的使用到该变量的线程:你们的缓冲区中的内容过期了,需要再重新刷新一下。
以上面那个代码例子来说 ,这样定义语句 volatile boolean flag = true;
当 main 线程修改主内存中 flag 的值时。这个时候, t1 线程,就会去重新读一遍主存,刷新自己的缓冲区(工作空间)。此时 t1 线程中的 flag 刷新为 false ,故循环停止。程序运行完毕。
虽然 volatile 用起来比较简单,但是该关键字背后代表的逻辑还是很深的。
还有个点要注意:
volatile并不能保证多个线程共同修改某一变量时所带来的不一致问题,也就是说 volatile 不能代替 synchronized。
当然, volatile 的效率高 synchronized 很多倍。但是该上锁的时候也只能上锁。
synchronized 是 既有可见性,又保证原子性。而 volatile 只保证可见性。synchronized的实现也是比 volatile 更重的。
关于 synchronized 本来也有一篇文章要写的。想想还是算了,本来想写点加锁释放锁的原理、可重入性质和可见性和原理啥的。
不过我过了归纳的那一段时间了,现在想归纳总结一通怪麻烦的。反正这些知识脑子里都有,就不记录了。
volatile 同时也有禁止指令重排序的功能