Java多執行緒學習(三)volatile關鍵字

Guide哥發表於2018-03-25

轉載請備註地址:https://juejin.im/post/5ab70c996fb9a028c812d06f

系列文章傳送門:

Java多執行緒學習(一)Java多執行緒入門

Java多執行緒學習(二)synchronized關鍵字(1)

Java多執行緒學習(二)synchronized關鍵字(2)

Java多執行緒學習(三)volatile關鍵字

Java多執行緒學習(四)等待/通知(wait/notify)機制

系列文章將被優先更新於微信公眾號“Java面試通關手冊”,歡迎廣大Java程式設計師和愛好技術的人員關注。

本節思維導圖:

volatile關鍵字

思維導圖原始檔+思維導圖軟體關注微信公眾號:**“Java面試通關手冊”回覆關鍵字:“Java多執行緒”**免費領取。

一 簡介

先來看看維基百科對“volatile關鍵字”的定義:

在程式設計中,尤其是在C語言、C++、C#和Java語言中,使用volatile關鍵字宣告的變數或物件通常具有與優化、多執行緒相關的特殊屬性。通常,volatile關鍵字用來阻止(偽)編譯器認為的無法“被程式碼本身”改變的程式碼(變數/物件)進行優化。如在C語言中,volatile關鍵字可以用來提醒編譯器它後面所定義的變數隨時有可能改變,因此編譯後的程式每次需要儲存或讀取這個變數的時候,都會直接從變數地址中讀取數。如果沒有volatile關鍵字,則編譯器可能優化讀取和儲存,可能暫時使用暫存器中的值,如果這個變數由別的程式更新了的話,將出現不一致的現象。據

在C環境中,volatile關鍵字的真實定義和適用範圍經常被誤解。雖然C++、C#和Java都保留了C中的volatile關鍵字,但在這些程式語言中volatile的用法和語義卻大相徑庭。

Java中的“volatile關鍵字”關鍵字:

在 JDK1.2 之前,Java的記憶體模型實現總是從主存(即共享記憶體)讀取變數,是不需要進行特別的注意的。而在當前的 Java 記憶體模型下,執行緒可以把變數儲存本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致

資料的不一致
要解決這個問題,就需要把變數宣告為 volatile,這就指示 JVM,這個變數是不穩定的,每次使用它都到主存中進行讀取。
volatile關鍵字的可見性

二 volatile關鍵字的可見性

volatile 修飾的成員變數在每次被執行緒訪問時,都強迫從主存(共享記憶體)中重讀該成員變數的值。而且,當成員變數發生變化時,強迫執行緒將變化值回寫到主存(共享記憶體)。這樣在任何時刻,兩個不同的執行緒總是看到某個成員變數的同一個值,這樣也就保證了同步資料的可見性

RunThread.java

 private boolean isRunning = true;
 int m;
	public boolean isRunning() {
		return isRunning;
	}
	public void setRunning(boolean isRunning) {
		this.isRunning = isRunning;
	}
	@Override
	public void run() {
		System.out.println("進入run了");
		while (isRunning == true) {
			int a=2;
			int b=3;
			int c=a+b;
			m=c;
		}
		System.out.println(m);
		System.out.println("執行緒被停止了!");
	}
}
複製程式碼

Run.java

public class Run {
	public static void main(String[] args) throws InterruptedException {
		RunThread thread = new RunThread();
		
		thread.start();
		Thread.sleep(1000);
		thread.setRunning(false);

		System.out.println("已經賦值為false");
	}
}
複製程式碼

執行結果:

死迴圈

RunThread類中的isRunning變數沒有加上volatile關鍵字時,執行以上程式碼會出現死迴圈,這是因為isRunning變數雖然被修改但是沒有被寫到主存中,這也就導致該執行緒在本地記憶體中的值一直為true,這樣就導致了死迴圈的產生。

解決辦法也很簡單:isRunning變數前加上volatile關鍵字即可。

isRunning變數前加上volatile關鍵字
這樣執行就不會出現死迴圈了。 加上volatile關鍵字後的執行結果:
加上volatile關鍵字後的執行結果

你是不是以為到這就完了?

不存在的!!!(這裡還有一點需要強調,下面的內容一定要看,不然你在用volatile關鍵字時會很迷糊,因為書籍幾乎都沒有提這個問題)

假如你把while迴圈程式碼里加上任意一個輸出語句或者sleep方法你會發現死迴圈也會停止,不管isRunning變數是否被加上了上volatile關鍵字。

加上輸出語句:

	while (isRunning == true) {
			int a=2;
			int b=3;
			int c=a+b;
			m=c;
			System.out.println(m);
		}
複製程式碼

加上sleep方法:

		while (isRunning == true) {
			int a=2;
			int b=3;
			int c=a+b;
			m=c;
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
複製程式碼

這是為什麼呢?

因為:JVM會盡力保證記憶體的可見性,即便這個變數沒有加同步關鍵字。換句話說,只要CPU有時間,JVM會盡力去保證變數值的更新。這種與volatile關鍵字的不同在於,volatile關鍵字會強制的保證執行緒的可見性。而不加這個關鍵字,JVM也會盡力去保證可見性,但是如果CPU一直有其他的事情在處理,它也沒辦法。最開始的程式碼,一直處於死迴圈中,CPU處於一直佔用的狀態,這個時候CPU沒有時間,JVM也不能強制要求CPU分點時間去取最新的變數值。而加了輸出或者sleep語句之後,CPU就有可能有時間去保證記憶體的可見性,於是while迴圈可以被終止

三 volatile關鍵字能保證原子性嗎?

《Java併發程式設計藝術》這本書上說保證但是在自增操作(非原子操作)上不保證,《Java多執行緒程式設計核心藝術》這本書說不保證。

我個人更傾向於這種說法:volatile無法保證對變數原子性的。我個人感覺《Java併發程式設計藝術》這本書上說volatile關鍵字保證原子性嗎但是在自增操作(非原子操作)上不保證這種說法是有問題的。只是個人看法,希望不要被噴。可以看下面測試程式碼:

MyThread.java

public class MyThread extends Thread {
	volatile public static int count;

	private static void addCount() {
		for (int i = 0; i < 100; i++) {
			count=i;
		}
		System.out.println("count=" + count);

	}
	@Override
	public void run() {
		addCount();
	}
}
複製程式碼

Run.java

public class Run {
	public static void main(String[] args) {
		MyThread[] mythreadArray = new MyThread[100];
		for (int i = 0; i < 100; i++) {
			mythreadArray[i] = new MyThread();
		}

		for (int i = 0; i < 100; i++) {
			mythreadArray[i].start();
		}
	}

}
複製程式碼

執行結果:

上面的“count=i;”是一個原子操作,但是執行結果大部分都是正確結果99,但是也有部分不是99的結果。

執行結果
解決辦法

使用synchronized關鍵字加鎖。(這只是一種方法,Lock和AtomicInteger原子類都可以,因為之前學過synchronized關鍵字,所以我們使用synchronized關鍵字的方法)

修改MyThread.java如下:

public class MyThread extends Thread {
	public static int count;

	synchronized private static void addCount() {
		for (int i = 0; i < 100; i++) {
			count=i;
		}
		System.out.println("count=" + count);
	}
	@Override
	public void run() {
		addCount();
	}
}
複製程式碼

這樣執行輸出的count就都為99了,所以要保證資料的原子性還是要使用synchronized關鍵字

四 synchronized關鍵字和volatile關鍵字比較

  • volatile關鍵字是執行緒同步的輕量級實現,所以volatile效能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用於變數而synchronized關鍵字可以修飾方法以及程式碼塊。synchronized關鍵字在JavaSE1.6之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用synchronized關鍵字還是更多一些
  • 多執行緒訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
  • volatile關鍵字能保證資料的可見性,但不能保證資料的原子性。synchronized關鍵字兩者都能保證。
  • volatile關鍵字用於解決變數在多個執行緒之間的可見性,而synchronized關鍵字解決的是多個執行緒之間訪問資源的同步性。

參考:

《Java多執行緒程式設計核心技術》

《Java併發程式設計的藝術》

極客學院Java併發程式設計wiki: http://wiki.jikexueyuan.com/project/java-concurrency/volatile1.html

相關文章