volatile底层原理详解
volatile理解
关键字volatile可以说是java虚拟机提供的最轻量级的同步机制,将一个变量定义为volatile之后,它将具备两种特性,一是可见性,一是禁止指令重排序,下面我们就具体来说一下
物理机内存机制
在了解java内存模型的前提上,我们可以先理解一下物理机是如何处理高效并发的问题的。
处理器:算机系统的运算和控制核心,想要详细了解的可以再看一下百度百科
主内存:存放数据
高速缓存:处理器和主内存之间的缓冲,存放处理器运算所需要的数据,是主内存数据的复制
缓存一致性协议:为了解决缓存一致性问题,需要各个处理器访问缓存时都遵循的协议
1.为什么需要高速缓存?
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。–《深入理解java虚拟机》
我根据理解画出了如上的图,以及环节流转。处理器、高速缓存、主内存以及缓存一致性协议的交互也体现在了上面,理解了上面之后我们就来理解一下java的内存模型。
Java内存模型
在了解java内存模型的时候我们先了解8个原子操作的概念:
1.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。从主内存读取数据。
2.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。将读取的数据放入工作内存中。
3.use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。获取工作内存中的数据然后执行引擎处理数据
4.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。工作内存接收执行引擎处理的结果赋值给工作内存中的变量
5.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。把工作内存改变的值传回给主内存。
6.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。将工作内存传输的值写入主内存。
7.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。store的时候进行lock。
8.unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。write之后解锁。
然后加上这8个操作之后的图如下(以flag为例)
工作内存从主内存read到flag的值,然后通过load将flag存入工作内存之中,use操作之后执行引擎获取到flag值,进行操作之后,flag值变为true,然后通过assign操作将flag的值更新到工作内存之中,工作内存通过store存储flag变量,最后主内存将flag变量write,然后线程获取到的就是flag的最新值。
这样枯燥的解释可能过于乏味,我们通过一个例子来说明一下吧
不使用volatile的样例
public class demo02 {
/**
* 测试代码
*/
private static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
new Thread(new Tr_A()).start();
Thread.sleep(2000);
new Thread(new Tr_B()).start();
Thread.sleep(2000);
new Thread(new Tr_C()).start();
}
public static class Tr_A implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_A运行 flag="+flag);
while(!flag) {
// System.out.println(flag);
}
System.out.println("Tr_A运行结束 flag="+flag);
}
}
public static class Tr_B implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_B运行");
flag=true;
System.out.println("Tr_B运行结束");
}
}
public static class Tr_C implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_C运行flag="+flag);
}
}
}
执行结果
从执行结果可以看出来代码一直在运行,没有结束,那么分析一波。
1.线程A,线程A先执行,获取的flag为false,然后因为while循环而一直处于运行状态,2秒之后就开始执行tr_B也就是线程B
2.线程B获取flag变量之后执行了flag=true的操作,这个时候flag的值变为了true,然后值也被写入了主内存中,这点从Tr_C线程可以看出来
3.线程C是在线程B执行后执行的,此时主内存的值已经变化,所以线程C获取的flag的值为true
4.此时线程没有结束,这个就是因为线程A还是还是用的工作内存中的flag变量的值,没有重新从主内存中获取,也可以说是线程B对变量flag的改变对于线程A不可见
那么,既然出现了线程B对变量的更改对于线程A不可见那么如何解决呢,下面就进入我们要了解的volatile关键字了,请看下面的例子
volatile例子
public class demo02 {
/**
* 测试代码
*/
private static volatile boolean flag=false;
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
new Thread(new Tr_A()).start();
Thread.sleep(2000);
new Thread(new Tr_B()).start();
Thread.sleep(2000);
new Thread(new Tr_C()).start();
}
public static class Tr_A implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_A运行 flag="+flag);
while(!flag) {
// System.out.println(flag);
}
System.out.println("Tr_A运行结束 flag="+flag);
}
}
public static class Tr_B implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_B运行");
flag=true;
System.out.println("Tr_B运行结束");
}
}
public static class Tr_C implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("Tr_C运行flag="+flag);
}
}
}
执行结果
看一下这个时候,main已经执行结束了,这说明什么,说明了线程B对flag变量值的更改对于线程A是可见的。我们可以来参考一下《深入理解java虚拟机》书中说明的对于volatile变量的特殊规则。
1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
2.只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)。
3.假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
这里我们用到的就是第一二条规则,来保证可见性,每次对变量的修改都会立刻同步到主内存之中,然后线程A在使用的时候就会每次都要重新从主内存中获取值更新到主内存中,然后使用
volatile可以保证原子性吗?
我们可以测试一下同时进行十个线程,然后针对变量num进行1000次的+1操作,如果可以保证原子性的话结果就会是10000,而如果不能保证原子性,这个值就不是是10000.
public class demo03 {
/**
* volatile能保证原子性吗?
* @param args
*/
private static volatile int num=0;
private static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Thread[] threads=new Thread[10];
for(int i=0;i<threads.length;i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int j=0;j<1000;j++) increase();
}
});
threads[i].start();
}
/**
* 要保证上面的十个线程执行完毕之后,父线程才会继续执行。
* 使用join方法
*/
for(int i=0;i<threads.length;i++) {
threads[i].join();
}
System.out.println(num);
}
}
看一下三次的执行结果
可以看到三次结果都不相同,证明中间存在着无效的处理,为什么会出现这种情形呢,举个例子。
1.线程A获取到num=0的值后,放入自己工作内存之后,use操作执行num++,工作内存变为了1,我们知道有了volatile之后回立即把它放入主内存之中,但是如果在此期间线程B已经运行,并且获取到了主内存中的num=0的值,那么它也会和线程A一样执行num++
2.线程A开始把自己的值assign、store和write操作,这个时候其他线程的工作内存就会被失效掉,此时线程B已经进行了一次num++操作了,后面他也就指挥执行999次,当然这个只是一个例子,后面还会出现这样的情况,所以结果就不可能是10000了,也就说明了volatile不能保证原子性
下面我们就要让synchronized来保证原子性
使用synchronize保证原子性
public class demo03 {
/**
* volatile能保证原子性吗?
* @param args
*/
private static volatile int num=0;
private synchronized static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Thread[] threads=new Thread[10];
for(int i=0;i<threads.length;i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int j=0;j<1000;j++) increase();
}
});
threads[i].start();
}
/**
* 要保证上面的十个线程执行完毕之后,父线程才会继续执行。
* 使用join方法
*/
for(int i=0;i<threads.length;i++) {
threads[i].join();
}
System.out.println(num);
}
}
执行结果
为什么说他能保证原子性呢,因为increase方法被synchronized修饰,还有static就保证了每次指挥有一个线程进入increase方法,这样就保证了原子性
无序性的例子
public class demo05 {
private static int x=0,y=0;
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Set<String> set=new HashSet<String>();
Map rst=new HashMap();
for(int i=0;i<100000;i++) {
x=0;y=0;
rst.clear();
Thread ta=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int a=y;
x=1;
rst.put("a", a);
}
});
Thread tb=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int b=x;
y=1;
rst.put("b", b);
}
});
ta.start();
tb.start();
ta.join();
tb.join();
set.add("a="+rst.get("a")+",b="+rst.get("b"));
}
System.out.println(set);
}
}
运行结果
最后一种a=0 b=null的情况明显是被重排序导致的问题,这种情况只可能是tb被先执行了。然后是x=0和y=0,然后是ta被执行。
下面我们加入volatile之后
加入volatile的例子
public class demo05 {
private volatile static int x=0,y=0;
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Set<String> set=new HashSet<String>();
Map rst=new HashMap();
for(int i=0;i<100000;i++) {
x=0;y=0;
rst.clear();
Thread ta=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int a=y;
x=1;
rst.put("a", a);
}
});
Thread tb=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int b=x;
y=1;
rst.put("b", b);
}
});
ta.start();
tb.start();
ta.join();
tb.join();
set.add("a="+rst.get("a")+",b="+rst.get("b"));
}
System.out.println(set);
}
}
执行结果
这个例子就参考一下volatile的规则
假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
volatile有禁止指令重排序的语义。
总结
volatile具有可见性和有序性,但是不具备原子性,为了确保原子性可以让volatile和synchronized关键字搭配使用。对于不足的希望提出,再进行学习改进。
java内存模型(JMM)和java运行时数据区域(内存区域)区别:
1.JMM-java内存模型 和jvm内存区域(java运行时数据区域 :堆区,栈区,方法区,程序计数器)是完全不同的概念
2.JMM内存模型是处理多线程间线程通信的,而jvn内存区域是对象内存自动化管理