Java中線程同步的理解 - 其實應該叫做Java線程排隊


Java中線程同步的理解

我們可以在計算機上運行各種計算機軟件程序。每一個運行的程序可能包括多個獨立運行的線程(Thread)。 
線程(Thread)是一份獨立運行的程序,有自己專用的運行棧。線程有可能和其他線程共享一些資源,比如,內存,文件,數據庫等。 
當多個線程同時讀寫同一份共享資源的時候,可能會引起沖突。這時候,我們需要引入線程“同步”機制,即各位線程之間要有個先來后到,不能一窩蜂擠上去搶作一團。 
同步這個詞是從英文synchronize(使同時發生)翻譯過來的。我也不明白為什么要用這個很容易引起誤解的詞。既然大家都這么用,咱們也就只好這么將就。 
線程同步的真實意思和字面意思恰好相反。線程同步的真實意思,其實是“排隊”:幾個線程之間要排隊,一個一個對共享資源進行操作,而不是同時進行操作。

因此,關於線程同步,需要牢牢記住的第一點是:線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程“同步”執行。這可真是個無聊的繞口令。 



 
        




關於線程同步,需要牢牢記住的第二點是 “共享”這兩個字。只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那么就根本沒有同步的必要。 

 
        
同步鎖:

前面講了為什么要線程同步,下面我們就來看如何才能線程同步。 
線程同步的基本實現思路還是比較容易理解的。我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個線程獲取了這把鑰匙,才有權利訪問該共享資源。 
線程同步鎖這個模型看起來很直觀。但是,還有一個嚴峻的問題沒有解決,這個同步鎖應該加在哪里? 
當然是加在共享資源上了。反應快的讀者一定會搶先回答。 
沒錯,如果可能,我們當然盡量把同步鎖加在共享資源上。
一些比較完善的共享資源,比如,文件系統,數據庫系統等,自身都提供了比較完善的同步鎖機制。我們不用另外給這些資源加鎖,這些資源自己就有鎖。 但是,大部分情況下,我們在代碼中訪問的共享資源都是比較簡單的共享對象。這些對象里面沒有地方讓我們加鎖。 讀者可能會提出建議:為什么不在每一個對象內部都增加一個新的區域,專門用來加鎖呢?
這種設計理論上當然也是可行的。問題在於,線程同步的情況並不是很普遍。如果因為這小概率事件,在所有對象內部都開辟一塊鎖空間,將會帶來極大的空間浪費。得不償失。 於是,現代的編程語言的設計思路都是把同步鎖加在代碼段上。確切的說,是把同步鎖加在“訪問共享資源的代碼段”上。
這一點一定要記住,同步鎖不是加在共享資源上,而是加在訪問同一份共享資源的不同代碼段上的。
同步鎖加在代碼段上,就很好地解決了上述的空間浪費問題。但是卻增加了模型的復雜度,也增加了我們的理解難度。 
現在我們就來仔細分析“同步鎖加在代碼段上”的線程同步模型:
首先,我們已經解決了同步鎖加在哪里的問題。我們已經確定,同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。 
其次,我們要解決的問題是,我們應該在代碼段上加什么樣的鎖?
這個問題是重點中的重點。這是我們尤其要注意的問題:訪問同一份共享資源的不同代碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那么根本就起不到同步的作用,沒有任何意義。 這就是說,同步鎖本身也一定是多個線程之間的共享對象。
 
         
         
        
這里我們以當前最流行的Java語言為例。Java語言里面用synchronized關鍵字給代碼段加鎖。整個語法形式表現為 
synchronized(同步鎖) { 
// 訪問共享資源,需要同步的代碼段 
}

這里尤其要注意的就是,同步鎖本身一定要是共享的對象。

… f1() {

Object lock1 = new Object(); // 產生一個同步鎖

synchronized(lock1){ 
// 代碼段 A 
// 訪問共享資源 resource1 
// 需要同步 
} 
}

上面這段代碼沒有任何意義。因為那個同步鎖是在函數體內部產生的。每個線程調用這段代碼的時候,都會產生一個新的同步鎖。那么多個線程之間,使用的是不同的同步鎖。根本達不到同步的目的。 
同步代碼一定要寫成如下的形式,才有意義。

public static final Object lock1 = new Object();

… f1() {

synchronized(lock1){ // lock1 是公用同步鎖 
// 代碼段 A 
// 訪問共享資源 resource1 
// 需要同步 
}

你不一定要把同步鎖聲明為static或者public,但是你一定要保證相關的同步代碼之間,一定要使用同一個同步鎖。 

講到這里,你一定會好奇,這個同步鎖到底是個什么東西。為什么隨便聲明一個Object對象,就可以作為同步鎖?
因為確實任何一個Object Reference都可以作為同步鎖。
我們可以把Object Reference理解為對象在內存分配系統中的內存地址。
因此,要保證同步代碼段之間使用的是同一個同步鎖,我們就要保證這些同步代碼段的synchronized關鍵字使用的是同一個Object Reference,同一個內存地址。
這也是為什么我在前面的代碼中聲明lock1的時候,使用了final關鍵字,這就是為了保證lock1的Object Reference在整個系統運行過程中都保持不變。
...

信號量:

同步鎖模型只是最簡單的同步模型。同一時刻,只有一個線程能夠運行同步代碼。 
有的時候,我們希望處理更加復雜的同步模型,比如生產者/消費者模型、讀寫同步模型等。這種情況下,同步鎖模型就不夠用了。我們需要一個新的模型。這就是我們要講述的信號量模型。 
信號量模型的工作方式如下:線程在運行的過程中,可以主動停下來,等待某個信號量的通知;這時候,該線程就進入到該信號量的待召(Waiting)隊列當中;等到通知之后,再繼續運行。 
(1)等待某個信號量的通知 
public static final Object signal = new Object();

… f1() { 
synchronized(singal) { // 首先我們要獲取這個信號量。這個信號量同時也是一個同步鎖

// 只有成功獲取了signal這個信號量兼同步鎖之后,我們才可能進入這段代碼 
signal.wait(); // 這里要放棄信號量。本線程要進入signal信號量的待召(Waiting)隊列

// 可憐。辛辛苦苦爭取到手的信號量,就這么被放棄了

// 等到通知之后,從待召(Waiting)隊列轉到就緒(Ready)隊列里面 
// 轉到了就緒隊列中,離CPU核心近了一步,就有機會繼續執行下面的代碼了。 
// 仍然需要把signal同步鎖競爭到手,才能夠真正繼續執行下面的代碼。命苦啊。 
… 
} 
}

(2)發出某個信號量的通知 
… f2() { 
synchronized(singal) { // 首先,我們同樣要獲取這個信號量。同時也是一個同步鎖。

// 只有成功獲取了signal這個信號量兼同步鎖之后,我們才可能進入這段代碼 
signal.notify(); // 這里,我們通知signal的待召隊列中的某個線程。

// 如果某個線程等到了這個通知,那個線程就會轉到就緒隊列中 
// 但是本線程仍然繼續擁有signal這個同步鎖,本線程仍然繼續執行 
// 嘿嘿,雖然本線程好心通知其他線程, 
// 但是,本線程可沒有那么高風亮節,放棄到手的同步鎖 
// 本線程繼續執行下面的代碼 
… 
} 
}

...
完整版:http://blog.csdn.net/u012179540/article/details/40685207




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM