Java关键字volatile的理解

阅读原文

摘要: 一.导读 在《Java内存模型的理解》一文中,我们提到了volatile关键字可以保证可见性,今天我们来聊聊这个volatile关键字。二.volatile深入解析 其实对内存模型有了一定的了解后,我们对volatile的理解就容易多了,volatile可以实现可见性、有序性,但是无法实现原子性。

一.导读
在《Java内存模型的理解》一文中,我们提到了volatile关键字可以保证可见性,今天我们来聊聊这个volatile关键字。
二.volatile深入解析
其实对内存模型有了一定的了解后,我们对volatile的理解就容易多了,volatile可以实现可见性、有序性,但是无法实现原子性。volatile的登场就是想解决在并发访问中,读取和更新变量的时候,要直接对主内存进行操作,而不是先操作自己的工作内存,然后在更新主内存这样的流程,用volatile修饰的变量会强制将assign赋值操作和store、write操作绑定在一起,将use使用操作强制和read、load操作绑定在一起,这样assign之后必须执行store、write操作,use操作之前必须先执行read、load操作。
1.可见性
volatile之所以能做到从主存中读写数据,是因为在并发过程中一个线程对volatile变量进行了修改操作后,会先写到工作内存,通过《Java内存模型的理解》中的硬件内存架构与Java内存模型关系图中,我们了解到,底层其实就是保存到CPU高速缓存中,这样会触发一个LOCK指令,这个指令会进行如下操作:
(1).锁定总线或者缓存,将修改后的新值保存到内存RAM中,也就是JVM关系图中对应的主内存中。
(2).会将其他CPU的高速缓存中的这个变量的值设置为无效,也就是JVM关系图中对应的工作内存里保存的这个变量值设置为无效。
综上两个操作,其他线程想要对变量进行操作时,读取变量时发现自己工作内存中的值是无效的,就从主内存重新读取,并保存到工作内存,这样就达到了可见性。
2.原子性
volatile对变量的操作是不具有原子性的,这里有一个经典的例子。
(1).非原子性

package cn.xiangquba;
public class volatileDemo {
	public volatile int x = 0;
	public void create() {
		x++;
	}
	public static void main(String[] args) {
		volatileDemo instance = new volatileDemo();
		for (int i = 0; i < 5; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 10; j++) {
						instance.create();
					}
				};
			}.start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(instance.x);
	}
}

(2).AtomicInteger实现原子性

package cn.xiangquba;
import java.util.concurrent.atomic.AtomicInteger;
public class volatileDemo {
	AtomicInteger x = new AtomicInteger();
	public void create() {
		x.incrementAndGet();
	}
	public static void main(String[] args) {
		volatileDemo instance = new volatileDemo();
		for (int i = 0; i < 5; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 10; j++) {
						instance.create();
					}
				};
			}.start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(instance.x);
	}
}

(3).Lock实现原子性

package cn.xiangquba;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class volatileDemo {
	public int x = 0;
	Lock lock = new ReentrantLock();
	public void create() {
		lock.lock();
		try {
			x++;
		} finally {
			lock.unlock();
		}
	}
	public static void main(String[] args) {
		volatileDemo instance = new volatileDemo();
		for (int i = 0; i < 5; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 10; j++) {
						instance.create();
					}
				};
			}.start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(instance.x);
	}
}

(4).synchronized实现原子性

package cn.xiangquba;
public class volatileDemo {
	public int x = 0;
	public synchronized void create() {
		x++;
	}
	public static void main(String[] args) {
		volatileDemo instance = new volatileDemo();
		for (int i = 0; i < 5; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 10; j++) {
						instance.create();
					}
				};
			}.start();
		}
		while (Thread.activeCount() > 1) {
			Thread.yield();
		}
		System.out.println(instance.x);
	}
}
3.有序性

再讲内存屏障的时候,提到了volatile底层是通过内存屏障的方式来禁止重排序,有三句很绕的话:

(1).当第二个操作为volatile写操做时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile之后;
(2).当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile之前;
(3).当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。
除以上三种情况以外可以进行重排序。另:以上三句话摘自《深入理解Java内存模型》
简单举个例子说明一下:

int x = 1;    //语句1
int y = 2;    //语句2
volatile boolean bflag = false;  //语句3
int m = 3;    //语句4
int n = 4;   //语句5

因为我们的变量bflag是用关键字volatile修饰,指令重排序的时候,语句1和语句2之间的顺序是无法保证的,同样语句4和语句5的顺序也是无法保证的,但是语句1和语句2一定在语句3前面,语句4和语句5一定在语句3后面。并且语句3执行的时候,语句1和语句2一定执行完毕。
三.参考文献
1.深入理解Java虚拟机
2.揭秘Java虚拟机-JVM设计原理与实现
3.深入理解Java内存模型

个人博客原文:https://www.xiangquba.cn/2018/03/03/java-volatile/


猜你喜欢

转载自blog.csdn.net/weixin_40581980/article/details/81000628