Java關鍵字volatile的理解

享叔發表於2018-03-03

一.導讀
在《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/


相關文章