1. 程式人生 > >淺析Java中volatile關鍵字及其作用

淺析Java中volatile關鍵字及其作用

在 Java 多執行緒中如何保證執行緒的安全性?那我們可以使用 Synchronized 同步鎖來給需要多個執行緒訪問的程式碼塊加鎖以保證執行緒安全性。使用 synchronized 雖然可以解決多執行緒安全問題,但弊端也很明顯:加鎖後多個執行緒需要判斷鎖,較為消耗資源。所以就引出我們今天的主角——volatile 關鍵字,一種輕量級的解決方案。

首先我們得了解量兩個概念:多執行緒和 JMM。

多執行緒

  • 程序和執行緒的概念
  • 建立執行緒的兩種方法
  • 執行緒的生命週期

Java 記憶體模型(JMM)

  • JMM 的概念
  • JMM 的結構組成部分

volatile 關鍵字作用

  • 記憶體可見性
  • 禁止指令重排

1、多執行緒

(1)程序和執行緒

程序:一個正在執行中的程式,動態的,是系統進行資源分配和排程的獨立單位。

執行緒:程序中一個獨立的控制單元,執行緒控制著程序的執行。一個程序中至少有一個執行緒。

(2)建立執行緒:(Thread 和 Runable)

繼承 Thread 類三步走:定義類繼承 Thread 類、重寫 run 方法、呼叫執行緒的 start 方法。

public class ThreadDemo {
	public static void main(String[] args) {
		// step2:建立該類的物件
		Lefthand left = new Lefthand();
		Righthand right = new Righthand();
		// step3:呼叫start方法啟動執行緒
		left.start();
		right.start();
	}
}

// step1:繼承Thread類,在子類中必須實現run方法
class Lefthand extends Thread {
	public void run() {
		for (int i = 0; i < 6; i++) {
			System.out.println("You are Students!");
			try {
				sleep(500);
			} catch (InterruptedException e) {
			}
		}
	}
}

class Righthand extends Thread {
	public void run() {
		for (int i = 0; i < 6; i++) {
			System.out.println("I am a Teacher!");
			try {
				sleep(300);
			} catch (InterruptedException e) {
			}
		}
	}
}

實現 Runable 介面三步走:定義類實現 Runable 介面、實現 run 方法、通過 Thread 類建立執行緒物件、start方法。

public class TwoThreadsDemo2 {
	public static void main(String[] args) {
		SimpleThread2 th1 = new SimpleThread2("Jack");
		SimpleThread2 th2 = new SimpleThread2("Tom");
		// step3
		Thread thread1 = new Thread(th1);
		Thread thread2 = new Thread(th2);
		thread1.start();
		thread2.start();

	}
}

// step1
class SimpleThread2 implements Runnable {
	String name;

	public SimpleThread2(String str) {
		name = str;
	}

	// step2
	public void run() {
		for (int i = 0; i < 8; i++) {
			System.out.println(i + " " + name);
			try {
				Thread.sleep((long) (Math.random() * 1000));
			} catch (InterruptedException e) {
			}
		}
		System.out.println("DONE!" + name);
	}
}

兩種方式的區別:

實現方式避免了單繼承的侷限性,執行緒程式碼存在介面子類的 run 方法中;繼承方式執行緒程式碼存放在 Thread 子類的 run 方法中。

(3)執行緒的生命週期:就緒狀態(執行緒 new 後)、可執行狀態(start 方法啟動執行緒,呼叫 run 方法)、阻塞狀態(sleep 方法 和 wait 方法)、死亡狀態(stop 方法)

2、Java 記憶體模型

(1)概念:Java 虛擬機器定義的一種抽象規範,使 Java 程式在不同平臺上的記憶體訪問效果一致。它決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。

(2)結構組成:(類比 CPU、快取記憶體 、記憶體 間的關係)


主記憶體:所有執行緒共享;共享變數在主記憶體中儲存的是其“本身”

工作記憶體:每個執行緒有自己的工作空間;共享變數在主記憶體中儲存的是其“副本”

執行緒對共享變數的所有操作全在工作記憶體中進行;每個執行緒只能訪問自己的工作記憶體;變數值的傳遞只能通過主記憶體完成。

3、volatile 關鍵字(用來修飾被不同執行緒訪問和修改的變數)

(1)記憶體可見性:

某執行緒對 volatile 變數的修改,對其他執行緒都是可見的。即獲取 volatile 變數的值都是最新的。

Java 中存在一種原則——先行發生原則(happens-before)。其表示兩個事件結果之間的關係:如果一個事件發生在另一個事件之間,其結果必須體現。volatile 的記憶體可見性就體現了該原則:對於一個 volatile 變數的寫操作先行發生於後面對這個變數的讀操作。

例:

volatile static int a = 0;
//執行緒 A 在其工作記憶體中寫入變數 a 的新值 1
a = 1 ;

//執行緒 B 在主記憶體中讀取變數 a 的值輸出
System.out.println(a);

需要注意的是 volatile 能保證記憶體的可見性,但不能保證變數的原子性

某一執行緒從主記憶體獲取到共享變數的值,當其修改完變數值重新寫入主記憶體時,並沒有去判斷主記憶體的值是否發生改變,有可能會出現意料之外的結果。

例如:當多個執行緒都對某一 volatile 變數(int a=0)進行 count++ 操作時,由於 count++ 操作並不是原子性操作,當執行緒 A 執行 count++ 後,A 工作記憶體其副本的值為 1,但執行緒執行時間到了,主記憶體的值仍為 0 ;執行緒 B又來執行 count++後並將值更新到主記憶體,主記憶體此時的值為 1;然後執行緒 A 繼續執行將值更新到主記憶體為 1,它並不知道執行緒 B 對變數進行了修改,也就是沒有判斷主記憶體的值是否發生改變,故最終結果為 1,但理論上 count++ 兩次,值應該為 2。

所以要使用 volatile 的記憶體可見性特性的話得滿足兩個條件:

  • 能確保只有單一的執行緒對共享變數的只進行修改。
  • 變數不需要和其他狀態變數共同參與不變的約束條件。

(2)禁止指令重排:

指令重排:JVM 在編譯 Java 程式碼時或 CPU 在執行 JVM 位元組碼時,對現有指令順序進行重新排序,優化程式的執行效率。(在不改變程式執行結果的前提下)

指令重排雖說可以優化程式的執行效率,但在多執行緒問題上會影響結果。那麼有什麼解決辦法呢?答案是記憶體屏障。記憶體屏障是一種屏障指令,使 CPU 或編譯器對屏障指令之前和之後發出的記憶體操作執行一個排序的約束。

四種類型:LoadLoad 屏障、StoreStore 屏障、LoadStore 屏障、StoreLoad 屏障。(Load 代表讀取指令、Store 代表寫入操作)

在 volatile 變數上的體現:(JVM 執行操作)

  • 在每個 volatile 寫入操作前插入 StoreStore 屏障;
  • 在寫操作後插入 StoreLoad 屏障;
  • 在讀操作前插入 LoadLoad 屏障;
  • 在讀操作後插入 LoadStore 屏障;

volatile 禁止指令重排在單例模式上有所體現,之前文章有所介紹(連結)。上邊介紹的操作只是針對 volatile 讀和 volatile 寫這種組合情況。還有其他的情況就不一一展開了。

總結:

(1)記憶體可見性的保證是基於屏障指令的。

(2)禁止指令重排在編譯時 JVM 編譯器遵循記憶體屏障的約束,執行時靠屏障指令組織重排。

(3)synchronized 關鍵字可以保證變數原子性和可見性;volatile 不能保證原子性。