volatile的使用

发表时间:2017-09-05 14:46:55 浏览量( 23 ) 留言数( 0 )

学习目标:

1、了解volatile的使用

2、什么情况下需要考虑线程安全

3、如何保证线程安全


教学过程

一、  volatile的作用的简介

    Java 同时也提供了一种更弱的、仅仅包含可见性的同步形式,并且只以volatile 关键字关联。volatile主要解决内存可见性和指令重排两个问题,我们先来简单了解一下这两个问题:

1. 内存可见性

    Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其JVM内存模型大致如下图。

attcontent/c5964e81-dc16-40a4-88dc-1729b3ed7f5f.png

而JAVA内存模型规定工作内存与主内存之间的交互协议,其中包括8种原子操作:


1) lock:将主内存中的变量锁定,为一个线程所独占 

2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量 

3) read:将主内存中的变量值读到工作内存当中 

4) load:将read读取的值保存到工作内存中的变量副本中。 

5) use:将值传递给线程的代码执行引擎 

6) assign:将执行引擎处理返回的值重新赋值给变量副本 

7) store:将变量副本的值存储到主内存中。 

8) write:将store存储的值写入到主内存的共享变量当中。


其中lock和unlock定义了一个线程访问一次共享内存的界限,而其它操作下线程的工作内存与主内存的交互大致如下图所示。


attcontent/d3f4265d-b9a5-4d26-99f4-6e90e7ef75a9.png

    从上图可以看出,read and load 主要是将主内存中数据复制到工作内存中,use and assign则主要是使用数据,并将改变后的值写入到工作内存,store and write则是用工作内存数据刷新主存相关内容。


    但是以上的一系列操作并不是原子的,也既是说在read and load之后,主内存中变量的值发生了改变,这时再use and assign则并不是取的最新的值,而我认为的内存可见性可粗略描述为,如果数据A在一个线程中的改变能够立即被其他线程可见,那么则说数据A具有内存可见性 ,也既是说如果数据A具有内存可见性,那么即使一个线程在read and load之后,数据A的值被改变了,在use and assign时也能获取到数据A最新的值并使用,那么该如何保证线程在每次use and assign时都是获取的数据A的最新的值呢?


    其实只要线程在每次use and assign时都是直接从主内存中获取数据A的值,就能够保证每次use and assign都是获取的数据A的最新的值,也既是能保证数据A的内存可见性,而volatile关键字的作用之一便是系统每次用到被它修饰过的变量时都是直接从主内存当中提取,而不是从Cache中提取,同时对于该变量的更改会马上刷新回主存,以使得各个线程取出的值相同,这里的Cache可以理解为线程的工作内存。当然了volatile关键字还有另外一个非常重要的作用,即局部阻止指令重排序。


2、指令重排

首先看下以下线程A和线程B的部分代码:


线程A:

content = initContent();    //(1)

isInit = true;              //(2)


线程B

while (isInit) {            //(3)

    content.operation();    //(4)

}


    从常规的理解来看,上面的代码是不会出问题的,但是JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑,也即是说对于线程A,代码(1)和代码(2)是不存在数据依赖性的,尽管代码(3)依赖于代码(2)的结果,但是由于代码(2)和代码(3)处于不同的线程之间,所以JVM可以不考虑线程B而对线程A中的代码(1)和代码(2)进行重排序,那么假设线程A中被重排序为如下顺序:


线程A:

isInit = true;              //(2)

content = initContent();    //(1)


    对于线程B,则可能在执行代码(4)时,content并没有被初始化,而造成程序错误。那么应该如何保证绝对的代码(2) happens-before 代码(3)呢?没错,仍然可以使用volatile关键字。


    volatile关键字除了之前提到的保证变量的内存可见性之外,另外一个重要的作用便是局部阻止重排序的发生,即保证被volatile关键字修饰的变量编译后的顺序与 也即是说如果对isInit使用了volatile关键字修饰,那么在线程A中,就能保证绝对的代码(1) happens-before 代码(2),也便不会出现因为重排序而可能造成的异常。


二、示例

    下面我们看一下一个例子,我们通过一个变量控制线程的终止,但是在一个多处理器的机器或多核单处理器的机器上,可能就看不到程序停止,因为每个处理器或者核心很可能有自己的一份stop的拷贝,当一条线程修改了自己的拷贝,其他线程的拷贝并没有被改变。

public class Thread3 implements Runnable{
	
	public   boolean stop;


	public void run() {
		while(!stop) {
			System.out.println("我一直在运行着。。。");
		}
		System.out.println("我已经停下来了。");

	}
	public void setStop(boolean flag) {
		this.stop=flag;
	}
	
}

    运行这个应用程序

public class Run3 {

	public static void main(String[] args) throws InterruptedException {
		
	    Thread3 run=new Thread3();
	    Thread thread=new Thread(run);
	    thread.start();
	    Thread.sleep(2000);
	    run.setStop(true);//尝试停止了他。
	   
	}

}

    在本地环境应该还是没有问题,但是如果你把他打包到生产环境就有可能会停不下来了。当然可以使用sys同步的机制解决这个问题,但是这样的效率就会比较低了。

	//加入volatile 保证可见性
	public volatile  boolean stop;


     volatile 的作用就是:由于stop己经标记为volatile ,每条线程都会访问主存中该变量的拷贝而不会访问缓存中的拷贝。这样,即使在多处理器或者多核的机器上,该程序也会停止。


三、总结

    最后,你可以将double 和long 型的属性声明成volatile,但是应该避免在32位的NM 上这样做,原因是此时访问一个double 或者long

型的变量值需要进行两步操作,若要安全地访问它们的佳,互斥(通过synchronized )是必要的。

    你会经常使用final 关键字来确保在不可变(不会发生改变〉类的上下文中线程的安全性。

  一定要注意volatile并不是线程安全的,不能保证原子性的操作。你定义了一个变量是volatile后执行i++等操作,还是会不安全的。