您的位置:首页 > 其它

volatile 关键字

2015-08-08 11:19 260 查看

1 volatile 的作用

Volatile 是Java中的一个关键字,仅用来修饰属性,不能修饰类和方法。它提供了一种弱的同步机制,用于多线程间的变量同步。它保证在某个线程中对变量的修改,可以立即被其他持有这个变量的线程看到。从内存模型的角度来说,就是工作内存中的改变立即刷新到主内存中,并强制其他工作内存进行刷新。

它可以保证代码的可见性、有序性,但不能保证原子性

2 volatile适用场景

目前比较常见的volatile的应用场景是使用volatile来修饰一个信号量控制线程的中断。目前JDK提供的线程中断方法stop()已经不建议被使用了,我们通常通过定义一个boolean类型的信号量控制线程的中断。

class MyThread implements Runnable
{
	private volatile boolean stopMark=false;
	@Override
	public void run()
	{
		System.out.println("thread start");
		while(!stopMark)
		{
			try
			{
				Thread.sleep(500);//模拟一个耗时的方法
			}
			catch (InterruptedException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		System.out.println("thread stop");
		
	}
	public void stop()
	{
		this.stopMark=true;
	}
}


有人会说,如果不使用volatile修饰的话,这个功能不能实现吗?的确,大多数情况下是可以实现的,而且笔者在学习volatile关键字之前也一直都是这样做的。但是极少数的情况下会出现问题,比如线程1调用线程2的stop的方法之后转入其他工作状态,改变的stopMark信号量没能及时同步回主内存,这样就可能导致线程2陷入死循环。为了避免这种情况发生,不仅要使用volatile关键字修饰信号量,同时还应该使用反馈机制,例如在while循环之后使用另外一个信号量标识线程已经退出等等

3 volatile能保证线程安全吗?

从前面的描述来看,似乎volatile可以保证操作的原子性来达到线程安全的目的。那么来看看下面这个例子:

public class Volatile
{
	public  volatile int a=0;
	
	public   void increase()
	{
		a++;
	}
	public static void main(String[] args)
	{
		final Volatile voilatile=new Volatile();
		for(int i=0;i<10;i++)
		{
			Thread thread=new Thread(new Runnable()
			{
				
				@Override
				public void run()
				{
					
					for(int j=0;j<1000;j++)
					{
						voilatile.increase();
					}
					
				}
			});
			thread.start();
		}
		while(Thread.activeCount()>1)
			Thread.yield();
		System.out.println(voilatile.a);
	}
}


从代码的角度来说加之对volatile的“线程安全”的理解,这段代码总能输出10000(1000*10)这个结果。但事实上,只有在极少的情况下能输出10000;对于大多数情况,这个输出都要比10000要小。这是什么原因呢?

从反汇编的结果可以看到,一个a++的操作,实际上对应的多条指令。在真正的add操作指令前后分别有get..和put..指令。当一个线程执行add指令的时候,其他多个线程也可能正在执行add指令,这样当发生put写入动作的时候,当前线程可能把一个较小的值写入覆盖掉其他多个线程更改的结果,这就写入了脏数据。例如当个get结果为100,add之后的结果为101,而其他线程也在add,这时主内存中的这个值可能已经是105,当把101同步回主内存的时候,这个值就变成了101,而并非我们期望的106…

由此看来volatile并不能保证操作的原子性。它只能保证操作的可见性。对于上面的代码可以通过同步块、加锁或者使用原子类的形式实现。对于volatile的适用场景,它必须满足以下条件:

运算结果不依赖与当前值,或者能保证只有一个线程可以修改这个值;

变量不需要与其他的状态变量共同参与变量约束

4 volatile保证代码有序性

由于JVM会对代码进行重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖关系赋值结果的地方都能够获得正确的结果,而不能保证执行的顺序与代码的顺序是一致的。下面来看一个例子,指令1把a+10,指令2把a扩大两倍,指令3把b扩大十倍。

a=a+10;//指令1
a=a*2; //指令2
b=b*10;//指令3


这里显然指令1、2存在依赖关系,(a+10)*2和a*2+10显然是不一样的,但是如果在指令1,2之间插入指令3,显然不会对结果构成影响。这就是指令重排序。而如果此时b被volatile关键字修饰,那么指令1,2永远在3之前执行,如果3后面存在其他指令,后面的指令也永远在3后执行。

这个到底有什么意义呢?试想这样一个场景,需要读取一段配置文件,设置一个标识符标识是否读取完成,例如下面伪代码:

volatile boolean isRead=false; //1
readContext();//2
isRead=true;//3


如果isRead变量不被volatile修饰的话,那么很可能3号代码提前到2号之前执行,这样就违反了我们的意图。

5 volatile的实现原理

这里主要讲volatile如何实现可见性和有序性的

5.1 可见性的实现

Java代码:

instance = new Singleton();//instance是volatile变量
汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
通过对带有volatile关键字修饰的java代码进行反编译,可以看到有一个lock指令。这个lock指令的有以下作用:

将当前处理器缓存行的数据会写回到系统内存,这个操作相当于发生了内存模型里的store和write操作。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

从这两句描述基本可以看出它是如何实现可见性。当对volatile关键字修饰的变量操作时,它会将这个操作结果立即同步会主内存,而其他线程的CPU缓存里(可以理解为工作内存)的数据被强制过期。而当CPU使用缓存数据的时候,首先要检验数据是否过期,如果过期就会从内存中重新读取。这样就实现了操作结果的可见性。

5.2 一致性的实现

这个实现也是利用前面的lock操作。Lock操作相当于在指令前增加了一个内存屏蔽,而指令重排序是无法跨越内存屏蔽的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: