JAVA 内存模型(二) volatile理解

    volatile 关键字是JAVA虚拟机提供的最轻量级的同步机制,在了解volatile的特性后,会让我们在多线程数据竞争方面更准确的使用。

  

一、概念解释

    一个volatile 变量,具备两种特性,第一是保证此变量多所有线程的可见性,这里的“可见性”是指当一个线程修改了该变量之后,新值对其他线程来说是可以立即得知的,而普通变量做不到,普通变量需要传递和主内存来完成,比如:线程A修改了普通变量的值,然后向主内存进行回写,另一条线程B在A线程回写完成之后再从主内存进行读取操作,变量的新值才会对线程B可见。(参考上一篇内存模型图)

 

    volatitle 变量的可见性描述是:“volatile变量对所有线程是立即可见的,对volatile 变量的所有写操作都能理解反应到其他线程中,也就是说volatile 变量再各个线程中是一致的。”这句话是对的,但是常常有点人就会通过上面描述得出:“基于volatile 变量在并发下是安全的。”这样的结论。volatile 变量再各个线程的工作内存中不存在一致性问题(因为volatile 变量再使用前,都会先刷新,即使刷新前不一致,执行引擎都是看到刷新后的值,因此可认为是一致的)。而Java 里面的运算并非原子性操作,导致volatile 变量的运算在并发下一样是不安全的,看代码:

package com;

/**
 * volatile 测试
 * @author Ran
 */
public class VolatileTest {
	public static volatile int race = 0;
	public  static void increase(){
		race ++ ;
	}
	// 线程数
	public static final int THREADS_COUNT = 20;
	
	public static void main(String[] args) {
		Thread[] threads = new Thread[THREADS_COUNT];
		for(int i = 0;i < THREADS_COUNT;i++){
			threads[i] = new Thread(new Runnable() {
				
				@Override
				public void run() {
					for(int i = 0;i<10000;i++){
						increase();
					}
				}
			});
			threads[i].start();
		}
		// 等待所有累加线程结束
		while(Thread.activeCount() > 1){
			Thread.yield();
		}
		System.out.println(race);
	}
	
	
}

 

以上代码理论结果应该是200000,但是你会发现值始终是小于这个数的,这是因为race 的自增运算不是原子性的,我们看反编译的字节码:

 

     从字节码看出,假设A线程执行getstatic 指令把race 的值取到操作栈顶时,volatile 保证了race 的的值此时是正确的假设现在是10,然后执行iconst_1 和iadd 的时候,可能另外B线程已经完成了上面的所有操作,值已经变成11了,这个时候A线程拿到的10 已经是过期数据,这时候A继续完成下面的操作的时候,即使增加了还是11(理论两个线程完成,会变成12)。在多线程情况下,累加的值也就小于预期了。

 

这里从上篇的内存模型来理解:

A,B 线程,分别从主内存拿到(read) volatile 变量race=0.然后放到(load)A,B的工作内存,这时候A线程把变量传递(use)给执行引擎,按字节码进行操作。同时B 执行同样的动作,由于JVM的不确定性,A在执行到iconst_1 和iadd 的时候,B已经执行完成,这时候A继续执行,最后刷新主内存的只1,结果就不是预期的了。

 

上述简单的解释:A,B 线程获取volatille 变量,每次都要从新从主内存读取,并且A线程改变了变量值,会告诉B线程告诉B线程,我已经改变了,你读取必须从主内存读取。但是在A线程改变,写入主内存和发送通知时,B线程获得的变量已经是主内存中读取的了,不需要从新读取,那么此时错误就产生了。 

 

 

二、解决指令从排序

       这里先看一段有趣的代码:

      

// 变量
	private  static boolean flag = false;;
	private  static int number;
	
	
	// 模拟初始化数字number
	private static class B extends Thread {
		@Override
        public void run() {	
			number = 100;
			flag = true;
        }
	}
	
	public static void main(String[] args) {
		new A().start();
		new B().start();
	}

	// 模拟获得B 初始化后的值
	private static class A extends Thread {
        @Override
        public void run() {
        	while(!flag){
        		System.out.println("A 线程 获得变量:"+number +":"+flag);
        		Thread.yield();
        	}
        	System.out.println("B 线程执行完成:"+number+":"+flag);
        }
    }

 

    上面操作是模拟多线程A线程要检测和获得B线程初始化Number 的值,也就是说B线程中当number = 100,flag = true .这是一个顺序操作。但是多次执行可能会得出这样的结果:
A 线程 获得变量:0:true
这里是A线程产生的,当A线程执行while(!flag) 为真,也就是flag = false.然后进入方法体,这时候B同时执行,将flag 变成了true,奇怪的是number 变量居然还是0.也就是说flag = true (可能)被提前执行了!!
但是我们的要求是flag = true 必须后于 number = 100 执行。JMM(JAVA内存模型) 为先限制这种指令重排序问题,加 入了volatile ,对于这两个操作的指令重排序问题,我仅仅找了一下规则,具体的还得根据JMM里面 的 8个基本操作入手,这里我转了一部分规则:
是否重排序              第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写  NO
volatile读NONONO
volatile写 NONO

我们以A,B操作,V,T变量,A:普通变量V 读/写  和 B:volatile变量T  写进行解释(第三行,最后一个格子)
如果A是对普通变量V进行读/写 操作。B对T 进行写操作,编译器不做重排序。
从表关系看出,我们的 number = 100,flag = true ,都有写操作。要想不重排序,我们可以在第二个变量flag 上加上volatile 就行了,但是给number 加上volatile 就不行,就是最后一行,第一个空格了,没效果。
 
小结:
        1.这里仅仅对volatile 进行概念性的理解
        2.关于重排序问题,涉及到内存和硬件、系统等差异,这里仅仅对JMM 这层进行介绍,关于volatile 对重排序的的原理,在上面贴出的链接有介绍。
        3.了解volatile 关键字,不至于我们乱用它,要想使用灵活准确,需要更加深入实践。
        4.个人理解对volatile 更多使用应该专注于它的可见性,多线程的情况下只允许一个线程有逻辑修改,而其他线程用于读取的一致性。当然在指令重排问题,在保证上一点的情况下,可以避免。
        
 
 
 
 
 
 
更多请参考:

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值