java并发之----volatile关键字

一、volatile

在java中,volatile关键字解决的是变量在多个线程之间的可见性,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

(1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

(2)禁止进行指令重排序。
注:不了解“原子性,可见性和有序性”的同学可以看下笔者之前的博客

1、volatile保证可见性

先看下下面的例子:

public class RunThread extends Thread {
	private boolean isRunning = true;
	public boolean isRunning(){
		return isRunning;
	}
	public void setRunning(boolean isRunning){
		this.isRunning = isRunning;
	}
	
	@Override
	public void run(){
		System.out.println("进入run了");
		while(isRunning){
		}
		System.out.println("线程被停止了");
	}
}
public class TestRunThread {
	public static void main(String[] args){
		try{
			RunThread thread = new RunThread();
			thread.start();
			Thread.sleep(1000);
			thread.setRunning(false);
			System.out.println("已经将Running设置为false");
		}catch(InterruptedException e){
			e.printStackTrace();
		}
	}
}

运行结果如下:
在这里插入图片描述

线程一直在私有堆栈中取得isRunning的值是true,而代码thread.setRunning(false);虽然被执行,但其更新的却是公共堆栈的isRunning变量值为false,所以出现了死循环的状态,代码System.out.println(“线程被停止了”);从未被执行。如下图:
在这里插入图片描述
这个问题其实就是私有堆栈(上图的的“工作内存”)中的值和公共堆栈(上图中的“主内存”)中的值不同步造成的。解决这样的问题就需要使用volatile关键字了,它的作用就是当线程访问isRunning这个变量是,强制性从公共堆栈中取值。
将RunThread.java的变量isRunning用volatile关键字修饰,其他不变:

private volatile boolean isRunning = true;

重新运行,结果如下:
在这里插入图片描述
通过使用volatile关键字,强制从公共内存中读取变量:
在这里插入图片描述
这个例子说明了volatile关键字可以保证可见性(这也是volatile最重要的功能)

2、volatile不保证原子性

volatile关键字虽然保证了变量在多线程之间的可见性,但它却不具备同步性,所以也就不具备原子性。看下面这个例子:

public class MyThread extends Thread{
	public volatile static int count;//volatile 修饰count
	private static void addCount(){
		for(int i=0; i<100; i++){
			count++;
		}
		System.out.println("count=" + count);
	}
	
	@Override
	public void run(){//实现run方法
		addCount();
	}
}
public class TestMyThread {
	public static void main(String[] args) {
		MyThread[] threadArray = new MyThread[100];
		for(int i=0; i<100; i++){
			threadArray[i] = new MyThread();//创建100个MyThread进程
		}
		for(int i=0; i<100; i++){
			threadArray[i].start();
		}
	}
}

运行结果:
在这里插入图片描述
更改MyThread.java的addCount方法,将其用synchronized修饰,其他不变,

private synchronized static void addCount()

重新运行,结果如下:
在这里插入图片描述

由此可以看出,volatile不保证原子性,否则运行结果应与synchronized修饰的结果一样。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。代码中的i++这种自增操作并不是一个原子操作,也就是非线程安全的。i++的操作步骤分解如下:
(1)从内存中取出 i 的值
(2)计算 i 的值
(3)将 i 的值写到内存中
假如在第(2)步计算值的时候,有另外一个进程修改了 i 的值,那么这个时候就会出现脏读(这就是上面运行结果count始终小于10000的原因)。解决的办法其实就是使用synchronized,所以说volatile本身并不处理数据的原子性,而是强制对数据的读写及时影响到主内存。

3、volatile在一定程度上保证有序性

volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

//x、y为非volatile变量
//flag为volatile变量
 
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

二、volatile的实现原理

volatile是怎么保证可见性和禁止指令重排序的?

下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

三、总结

下面将volatile与synchronized做一下比较总结:
(1)volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好

(2)volatile只能修饰变量,而synchronized可以修饰方法、代码块和类,新版本的jdk也对synchronized做了许多优化,开发中使用synchronized的比率比较大

(3)多线程访问volatile不会发生阻塞,而synchronized会发生阻塞

(4)volatile能保证可见性,但不能保证原子性,而synchronized都可以保证

(5)volatile解决的是变量在多线程之间的可见性,而synchronized解决的是多个线程之间访问资源的同步性。

四、参考

《深入理解java虚拟机》 周志明
《Java多线程编程核心技术》高洪岩

猜你喜欢

转载自blog.csdn.net/Felix_ar/article/details/84441938
今日推荐