1. 程式人生 > >Java併發程式設計(2)-共享物件解讀

Java併發程式設計(2)-共享物件解讀

文章目錄


在上一篇關於執行緒安全概述的文章中提到過,編寫正確的併發程式的關鍵在於對共享的、可變狀態的變數進行訪問管理,上一篇著重講解使用同步來避免多個執行緒在同一時間訪問同一資料;這一篇將講解共享和釋出物件的技術,使多個執行緒能夠安全地訪問他們。
本文內容均總結自《Java併發程式設計實踐》第三章 共享物件 章節的內容 ,詳情可以查閱該書。

一、變數可見性

1.1、共享物件的可見性

閱讀下面的程式碼,執行main方法後,會出現什麼結果?

public class ThreadTest{
	private static  boolean ready;
	private static  int i;
	
	public static void main(String[] args) {
		new MyThread().start();
		i = 1;
		ready = true;
	}
	
	public static class MyThread extends Thread{
		public void
run(){ if(!ready){ Thread.yield(); } System.out.println(i); } } }

main方法是寫入資料的執行緒,而MyThread是讀取資料的執行緒,由於沒有適當同步操作,兩個執行緒之間發生了讀寫的分離,而通常來說,當讀和寫分離為兩個執行緒時,不能保證讀執行緒能及時地讀取到其他執行緒寫入的值,甚至說不可能。為了保證讀寫執行緒的值能共享,必須使用同步機制。

1.2 、volatile變數

鎖不僅僅是關於同步和互斥的,也是關於記憶體可見的。為了保證所有的執行緒都能夠看到其共享的、可變變數的最新值,讀取和寫入執行緒必須使用公共的鎖進行同步。
Java語言也提供了其他的選擇,即一種同步的弱形式:volatile變數

。它確保變數的更新以可預見的方式告知其他執行緒。當一個域宣告為volatile型別後,編譯器與執行時會監視這個變數;volatile變數不會快取在暫存器或者快取在對其它處理器隱藏的地方。所以,讀一個volatile型別的變數時,總會返回由某一執行緒所寫入的最新值

以下的數羊程式簡單說明volatile變數。

private volatile boolean asleep;
	//是羊則數羊
	while(!asleep){
		countSomeSheep();
	}

volatile變數通常被當做標識完成、中斷、狀態的標記使用,如上面程式中的asleep標記。儘管volatile也可以用來標識其他型別的狀態資訊,但是必須十分小心。比如,volatile的語義不足以使自增查詢(count++)原子化,除非你能保證只有一個執行緒對變數執行寫查詢。原子變數(atomic variable)提供“讀-寫-改”的原子查詢的支援,而且常被用作更優的volatile變數

只有滿足了下面所有標準後才能使用volatile變數:

1、寫入變數時並不依賴變數的當前值;或者能夠保證只有單一的執行緒修改變	量的值;
2、變數不需要與其他的狀態變數共同參與不變約束;
3、訪問變數時,沒有其他的原因需要加鎖。

二、變數釋出與逸出

釋出一個物件的意思是使它能夠被當前範圍之外的程式碼所使用。比如將一個引用儲存到其他程式碼可以訪問到的地方。

2.1、如何釋出物件

最常見的釋出物件方式是將物件的引用儲存到公共靜態域中。任何類和執行緒都能夠看到這個域:

public static Set<Secret> knownSecrets;
	public void initialize(){
		knownSecrets = new HashSet<Secret>();
}

2.2、釋出物件帶來的this引用逸出

併發程式設計實踐中,this引用逃逸("this"escape)是指物件還沒有構造完成,它的this引用就被髮布出去了。這是危及到執行緒安全的,因為其他執行緒有可能通過這個逸出的引用訪問到“初始化了一半”的物件(partially-constructed object)。這樣就會出現某些執行緒中看到該物件的狀態是沒初始化完的狀態,而在另外一些執行緒看到的卻是已經初始化完的狀態,這種不一致性是不確定的,程式也會因此而產生一些無法預知的併發錯誤。( 詳情可閱讀《Java併發程式設計實踐》)

三、執行緒封閉

訪問共享的、可變的資料要求使用同步。一個可以避免同步的方式就是不共享資料。如果資料僅在單執行緒中被訪問到,就不需要任何同步。執行緒封閉(Thread confinement)技術是在實現執行緒安全的最簡單的方式之一。當物件封閉在一個執行緒中時,這種做法會自動成為執行緒安全的。

3.1、Ad-hoc 執行緒限制

Ad-hoc執行緒封閉是指,維護執行緒封閉性的職責完全由程式實現來承擔。Ad-hoc執行緒封閉是非常脆弱的,因為沒有任何一種語言特性,例如可見性修飾符或區域性變數,能將物件封閉到目標執行緒上。事實上,對執行緒封閉物件(例如,GUI應用程式中的視覺化元件或資料模型等)的引用通常儲存在公有變數中。(詳情可閱讀《Java併發程式設計實踐》)

3.2、使用 ThreadLocal

一種維護執行緒限制的更加規範的方式是使用TheadLocal,它允許將每個執行緒與持有數值的物件關聯在一起。ThreadLocal提供了get和set訪問器,為每個使用它的執行緒維護一份單獨的拷貝。所以get總是返回由當前執行執行緒通過set設定的最新值。(詳情可閱讀 http://www.cnblogs.com/dolphin0520/p/3920407.html)

四、如何安全地共享物件

為了安全地釋出物件,物件的引用以及物件的狀態必須同時對其他執行緒可見。一個正確建立的物件可以通過下列條件安全地釋出:

1、通過靜態初始化器初始化物件的引用;

2、將它的引用儲存到volatile域或AtomicReference;

3、將它的引用儲存到正確建立的物件的final域中;

4、將它的引用儲存到由鎖保護的域中。

在併發程式中,使用和共享物件的一些有效策略:

1、執行緒限制:一個執行緒限制的物件,通過限制線上程中,而被執行緒獨佔,並且只能被佔有它的執行緒修改。

2、共享只讀:一個共享的只讀物件,在沒有額外同步的情況下,可以被多個執行緒兵法地訪問,但是任何執行緒都不能修改它。

3、共享執行緒安全:一個執行緒安全的物件在內部進行同步,所以其他執行緒無須額外同步,就可以通過公共介面隨意地訪問它。

4、被守護的:一個被守護的物件只能通過特定的鎖來訪問。