1. 程式人生 > >執行緒如何實現同步和通訊

執行緒如何實現同步和通訊

執行緒同步


什麼是執行緒同步?

當使用多個執行緒來訪問同一個資料時,非常容易出現執行緒安全問題(比如多個執行緒都在操作同一資料導致資料不一致),所以我們用同步機制來解決這些問題。

實現同步機制有兩個方法:
1。同步程式碼塊:
synchronized(同一個資料){} 同一個資料:就是N條執行緒同時訪問一個數據。

2。

同步方法:
public synchronized 資料返回型別 方法名(){}
就是使用 synchronized 來修飾某個方法,則該方法稱為同步方法。對於同步方法而言,無需顯示指定同步監視器,同步方法的同步監視器是 this 也就是該物件的本身(這裡指的物件本身有點含糊,其實就是呼叫該同步方法的物件)

通過使用同步方法,可非常方便的將某類變成執行緒安全的類,具有如下特徵:
1,該類的物件可以被多個執行緒安全的訪問。
2,每個執行緒呼叫該物件的任意方法之後,都將得到正確的結果。
3,每個執行緒呼叫該物件的任意方法之後,該物件狀態依然保持合理狀態。
注:synchronized關鍵字可以修飾方法,也可以修飾程式碼塊,但不能修飾構造器,屬性等。

實現同步機制注意以下幾點:   安全性高,效能低,在多執行緒用。效能高,安全性低,在單執行緒用。
1,不要對執行緒安全類的所有方法都進行同步,只對那些會改變共享資源方法的進行同步。
2,如果可變類有兩種執行環境,當執行緒環境和多執行緒環境則應該為該可變類提供兩種版本:執行緒安全版本和執行緒不安全版本(沒有同步方法和同步塊)。在單執行緒中環境中,使用執行緒不安全版本以保證效能,在多執行緒中使用執行緒安全版本.

執行緒通訊:

為什麼要使用執行緒通訊?

當使用synchronized 來修飾某個共享資源時(分同步程式碼塊和同步方法兩種情況),當某個執行緒獲得共享資源的鎖後就可以執行相應的程式碼段,直到該執行緒執行完該程式碼段後才釋放對該共享資源的鎖,讓其他執行緒有機會執行對該共享資源的修改。當某個執行緒佔有某個共享資源的鎖時,如果另外一個執行緒也想獲得這把鎖執行就需要使用wait() 和notify()/notifyAll()方法來進行執行緒通訊了。
Java.lang.object 裡的三個方法wait() notify()  notifyAll()
wait方法導致當前執行緒等待,直到其他執行緒呼叫同步監視器的notify方法或notifyAll方法來喚醒該執行緒。
wait(mills)方法
都是等待指定時間後自動甦醒,呼叫wait方法的當前執行緒會釋放該同步監視器的鎖定,可以不用notify或notifyAll方法把它喚醒。

notify()
喚醒在同步監視器上等待的單個執行緒,如果所有執行緒都在同步監視器上等待,則會選擇喚醒其中一個執行緒,選擇是任意性的,只有當前執行緒放棄對該同步監視器的鎖定後,也就是使用wait方法後,才可以執行被喚醒的執行緒。

notifyAll()方法
喚醒在同步監視器上等待的所有的執行緒。只用當前執行緒放棄對該同步監視器的鎖定後,才可以執行被喚醒的執行緒。

-------------------------------------------------另外的總結2--------------------------------------------

執行緒的同步 
原子操作:根據Java規範,對於基本型別的賦值或者返回值操作,是原子操作。但這裡的基本資料型別不包括long和double, 因為JVM看到的基本儲存單位是32位,而long 和double都要用64位來表示。所以無法在一個時鐘週期內完成。 

自增操作(++)不是原子操作,因為它涉及到一次讀和一次寫。 

原子操作:由一組相關的操作完成,這些操作可能會操縱與其它的執行緒共享的資源,為了保證得到正確的運算結果,一個執行緒在執行原子操作其間,應該採取其他的措施使得其他的執行緒不能操縱共享資源。 

同步程式碼塊:為了保證每個執行緒能夠正常執行原子操作,Java引入了同步機制,具體的做法是在代表原子操作的程式程式碼前加上synchronized標記,這樣的程式碼被稱為同步程式碼塊。 

同步鎖:每個JAVA物件都有且只有一個同步鎖,在任何時刻,最多隻允許一個執行緒擁有這把鎖。 

當一個執行緒試圖訪問帶有synchronized(this)標記的程式碼塊時,必須獲得 this關鍵字引用的物件的鎖,在以下的兩種情況下,本執行緒有著不同的命運。 
1、 假如這個鎖已經被其它的執行緒佔用,JVM就會把這個執行緒放到本物件的鎖池中。本執行緒進入阻塞狀態。鎖池中可能有很多的執行緒,等到其他的執行緒釋放了鎖,JVM就會從鎖池中隨機取出一個執行緒,使這個執行緒擁有鎖,並且轉到就緒狀態。 
2、 假如這個鎖沒有被其他執行緒佔用,本執行緒會獲得這把鎖,開始執行同步程式碼塊。 (一般情況下在執行同步程式碼塊時不會釋放同步鎖,但也有特殊情況會釋放物件鎖 如在執行同步程式碼塊時,遇到異常而導致執行緒終止,鎖會被釋放;在執行程式碼塊時,執行了鎖所屬物件的wait()方法,這個執行緒會釋放物件鎖,進入物件的等待池中) 

執行緒同步的特徵: 
1、 如果一個同步程式碼塊和非同步程式碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因為當一個執行緒執行一個物件的同步程式碼塊時,其他的執行緒仍然可以執行物件的非同步程式碼塊。(所謂的執行緒之間保持同步,是指不同的執行緒在執行同一個物件的同步程式碼塊時,因為要獲得物件的同步鎖而互相牽制) 
2、 每個物件都有唯一的同步鎖 
3、 在靜態方法前面可以使用synchronized修飾符。 
4、 當一個執行緒開始執行同步程式碼塊時,並不意味著必須以不間斷的方式執行,進入同步程式碼塊的執行緒可以執行Thread.sleep()或執行Thread.yield()方法,此時它並不釋放物件鎖,只是把執行的機會讓給其他的執行緒。 
5、 Synchronized宣告不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。 

執行緒安全的類: 
1、 這個類的物件可以同時被多個執行緒安全的訪問。 
2、 每個執行緒都能正常的執行原子操作,得到正確的結果。 
3、 在每個執行緒的原子操作都完成後,物件處於邏輯上合理的狀態。 

釋放物件的鎖: 
1、 執行完同步程式碼塊就會釋放物件的鎖 
2、 在執行同步程式碼塊的過程中,遇到異常而導致執行緒終止,鎖也會被釋放 
3、 在執行同步程式碼塊的過程中,執行了鎖所屬物件的wait()方法,這個執行緒會釋放物件鎖,進入物件的等待池。 

死鎖 
當一個執行緒等待由另一個執行緒持有的鎖,而後者正在等待已被第一個執行緒持有的鎖時,就會發生死鎖。JVM不監測也不試圖避免這種情況,因此保證不發生死鎖就成了程式設計師的責任。 

如何避免死鎖 
一個通用的經驗法則是:當幾個執行緒都要訪問共享資源A、B、C 時,保證每個執行緒都按照同樣的順序去訪問他們。 

執行緒通訊 
Java.lang.Object類中提供了兩個用於執行緒通訊的方法 
1、 wait():執行了該方法的執行緒釋放物件的鎖,JVM會把該執行緒放到物件的等待池中。該執行緒等待其它執行緒喚醒 
2、 notify():執行該方法的執行緒喚醒在物件的等待池中等待的一個執行緒,JVM從物件的等待池中隨機選擇一個執行緒,把它轉到物件的鎖池中。

----------------------------------------------------執行緒同步的總結3---------------------------------------

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

因此,關於執行緒同步,需要牢牢記住的第一點是:執行緒同步就是執行緒排隊。同步就是排隊。執行緒同步的目的就是避免執行緒“同步”執行。這可真是個無聊的繞口令。 
關於執行緒同步,需要牢牢記住的第二點是 “共享”這兩個字。只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那麼就根本沒有同步的必要。 
關於執行緒同步,需要牢牢記住的第三點是,只有“變數”才需要同步訪問。如果共享的資源是固定不變的,那麼就相當於“常量”,執行緒同時讀取常量也不需要同步。至少一個執行緒修改共享資源,這樣的情況下,執行緒之間就需要同步。 
關於執行緒同步,需要牢牢記住的第四點是:多個執行緒訪問共享資源的程式碼有可能是同一份程式碼,也有可能是不同的程式碼;無論是否執行同一份程式碼,只要這些執行緒的程式碼訪問同一份可變的共享資源,這些執行緒之間就需要同步。 

為了加深理解,下面舉幾個例子。 
有兩個採購員,他們的工作內容是相同的,都是遵循如下的步驟: 
(1)到市場上去,尋找併購買有潛力的樣品。 
(2)回到公司,寫報告。 
這兩個人的工作內容雖然一樣,他們都需要購買樣品,他們可能買到同樣種類的樣品,但是他們絕對不會購買到同一件樣品,他們之間沒有任何共享資源。所以,他們可以各自進行自己的工作,互不干擾。 
這兩個採購員就相當於兩個執行緒;兩個採購員遵循相同的工作步驟,相當於這兩個執行緒執行同一段程式碼。 

下面給這兩個採購員增加一個工作步驟。採購員需要根據公司的“布告欄”上面公佈的資訊,安排自己的工作計劃。
這兩個採購員有可能同時走到布告欄的前面,同時觀看布告欄上的資訊。這一點問題都沒有。因為布告欄是隻讀的,這兩個採購員誰都不會去修改布告欄上寫的資訊。 

下面增加一個角色。一個辦公室行政人員這個時候,也走到了布告欄前面,準備修改布告欄上的資訊。 
如果行政人員先到達布告欄,並且正在修改布告欄的內容。兩個採購員這個時候,恰好也到了。這兩個採購員就必須等待行政人員完成修改之後,才能觀看修改後的資訊。 
如果行政人員到達的時候,兩個採購員已經在觀看布告欄了。那麼行政人員需要等待兩個採購員把當前資訊記錄下來之後,才能夠寫上新的資訊。 
上述這兩種情況,行政人員和採購員對布告欄的訪問就需要進行同步。因為其中一個執行緒(行政人員)修改了共享資源(布告欄)。而且我們可以看到,行政人員的工作流程和採購員的工作流程(執行程式碼)完全不同,但是由於他們訪問了同一份可變共享資源(布告欄),所以他們之間需要同步。 

同步鎖 

前面講了為什麼要執行緒同步,下面我們就來看如何才能執行緒同步。 
執行緒同步的基本實現思路還是比較容易理解的。我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個執行緒獲取了這把鑰匙,才有權利訪問該共享資源。 
生活中,我們也可能會遇到這樣的例子。一些超市的外面提供了一些自動儲物箱。每個儲物箱都有一把鎖,一把鑰匙。人們可以使用那些帶有鑰匙的儲物箱,把東西放到儲物箱裡面,把儲物箱鎖上,然後把鑰匙拿走。這樣,該儲物箱就被鎖住了,其他人不能再訪問這個儲物箱。(當然,真實的儲物箱鑰匙是可以被人拿走複製的,所以不要把貴重物品放在超市的儲物箱裡面。於是很多超市都採用了電子密碼鎖。) 
執行緒同步鎖這個模型看起來很直觀。但是,還有一個嚴峻的問題沒有解決,這個同步鎖應該加在哪裡? 
當然是加在共享資源上了。反應快的讀者一定會搶先回答。 
沒錯,如果可能,我們當然儘量把同步鎖加在共享資源上。一些比較完善的共享資源,比如,檔案系統,資料庫系統等,自身都提供了比較完善的同步鎖機制。我們不用另外給這些資源加鎖,這些資源自己就有鎖。 
但是,大部分情況下,我們在程式碼中訪問的共享資源都是比較簡單的共享物件。這些物件裡面沒有地方讓我們加鎖。 
讀者可能會提出建議:為什麼不在每一個物件內部都增加一個新的區域,專門用來加鎖呢?這種設計理論上當然也是可行的。問題在於,執行緒同步的情況並不是很普遍。如果因為這小概率事件,在所有物件內部都開闢一塊鎖空間,將會帶來極大的空間浪費。得不償失。 
於是,現代的程式語言的設計思路都是把同步鎖加在程式碼段上。確切的說,是把同步鎖加在“訪問共享資源的程式碼段”上。這一點一定要記住,同步鎖是加在程式碼段上的。 
同步鎖加在程式碼段上,就很好地解決了上述的空間浪費問題。但是卻增加了模型的複雜度,也增加了我們的理解難度。 
現在我們就來仔細分析“同步鎖加在程式碼段上”的執行緒同步模型。 
首先,我們已經解決了同步鎖加在哪裡的問題。我們已經確定,同步鎖不是加在共享資源上,而是加在訪問共享資源的程式碼段上。 
其次,我們要解決的問題是,我們應該在程式碼段上加什麼樣的鎖。這個問題是重點中的重點。這是我們尤其要注意的問題:訪問同一份共享資源的不同程式碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那麼根本就起不到同步的作用,沒有任何意義。 
這就是說,同步鎖本身也一定是多個執行緒之間的共享物件。 

Java語言的synchronized關鍵字 

為了加深理解,舉幾個程式碼段同步的例子。 
不同語言的同步鎖模型都是一樣的。只是表達方式有些不同。這裡我們以當前最流行的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物件,就可以作為同步鎖? 
在Java裡面,同步鎖的概念就是這樣的。任何一個Object Reference都可以作為同步鎖。我們可以把Object Reference理解為物件在記憶體分配系統中的記憶體地址。因此,要保證同步程式碼段之間使用的是同一個同步鎖,我們就要保證這些同步程式碼段的synchronized關鍵字使用的是同一個Object Reference,同一個記憶體地址。這也是為什麼我在前面的程式碼中宣告lock1的時候,使用了final關鍵字,這就是為了保證lock1的Object Reference在整個系統執行過程中都保持不變。 
一些求知慾強的讀者可能想要繼續深入瞭解synchronzied(同步鎖)的實際執行機制。Java虛擬機器規範中(你可以在google用“JVM Spec”等關鍵字進行搜尋),有對synchronized關鍵字的詳細解釋。synchronized會編譯成 monitor enter, … monitor exit之類的指令對。Monitor就是實際上的同步鎖。每一個Object Reference在概念上都對應一個monitor。 
這些實現細節問題,並不是理解同步鎖模型的關鍵。我們繼續看幾個例子,加深對同步鎖模型的理解。 

public static final Object lock1 = new Object(); 

… f1() { 

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



… f2() { 

synchronized(lock1){ // lock1 是公用同步鎖 
  // 程式碼段 B 
// 訪問共享資源 resource1 
// 需要同步 



上述的程式碼中,程式碼段A和程式碼段B就是同步的。因為它們使用的是同一個同步鎖lock1。 
如果有10個執行緒同時執行程式碼段A,同時還有20個執行緒同時執行程式碼段B,那麼這30個執行緒之間都是要進行同步的。 
這30個執行緒都要競爭一個同步鎖lock1。同一時刻,只有一個執行緒能夠獲得lock1的所有權,只有一個執行緒可以執行程式碼段A或者程式碼段B。其他競爭失敗的執行緒只能暫停執行,進入到該同步鎖的就緒(Ready)佇列。 
每一個同步鎖下面都掛了幾個執行緒佇列,包括就緒(Ready)佇列,待召(Waiting)佇列等。比如,lock1對應的就緒佇列就可以叫做lock1 - ready queue。每個佇列裡面都可能有多個暫停執行的執行緒。 
注意,競爭同步鎖失敗的執行緒進入的是該同步鎖的就緒(Ready)佇列,而不是後面要講述的待召佇列(Waiting Queue,也可以翻譯為等待佇列)。就緒佇列裡面的執行緒總是時刻準備著競爭同步鎖,時刻準備著執行。而待召佇列裡面的執行緒則只能一直等待,直到等到某個訊號的通知之後,才能夠轉移到就緒佇列中,準備執行。 
成功獲取同步鎖的執行緒,執行完同步程式碼段之後,會釋放同步鎖。該同步鎖的就緒佇列中的其他執行緒就繼續下一輪同步鎖的競爭。成功者就可以繼續執行,失敗者還是要乖乖地待在就緒佇列中。 
因此,執行緒同步是非常耗費資源的一種操作。我們要儘量控制執行緒同步的程式碼段範圍。同步的程式碼段範圍越小越好。我們用一個名詞“同步粒度”來表示同步程式碼段的範圍。 
同步粒度 
在Java語言裡面,我們可以直接把synchronized關鍵字直接加在函式的定義上。 
比如。 
… synchronized … f1() { 
  // f1 程式碼段 


這段程式碼就等價於 
… f1() { 
  synchronized(this){ // 同步鎖就是物件本身 
    // f1 程式碼段 
  } 


同樣的原則適用於靜態(static)函式 
比如。 
… static synchronized … f1() { 
  // f1 程式碼段 


這段程式碼就等價於 
…static … f1() { 
  synchronized(Class.forName(…)){ // 同步鎖是類定義本身 
    // f1 程式碼段 
  } 


但是,我們要儘量避免這種直接把synchronized加在函式定義上的偷懶做法。因為我們要控制同步粒度。同步的程式碼段越小越好。synchronized控制的範圍越小越好。 
我們不僅要在縮小同步程式碼段的長度上下功夫,我們同時還要注意細分同步鎖。 
比如,下面的程式碼 

public static final Object lock1 = new Object(); 

… f1() { 

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



… f2() { 

synchronized(lock1){ // lock1 是公用同步鎖 
  // 程式碼段 B 
// 訪問共享資源 resource1 
// 需要同步 



… f3() { 

synchronized(lock1){ // lock1 是公用同步鎖 
  // 程式碼段 C 
// 訪問共享資源 resource2 
// 需要同步 



… f4() { 

synchronized(lock1){ // lock1 是公用同步鎖 
  // 程式碼段 D 
// 訪問共享資源 resource2 
// 需要同步 



上述的4段同步程式碼,使用同一個同步鎖lock1。所有呼叫4段程式碼中任何一段程式碼的執行緒,都需要競爭同一個同步鎖lock1。 
我們仔細分析一下,發現這是沒有必要的。 
因為f1()的程式碼段A和f2()的程式碼段B訪問的共享資源是resource1,f3()的程式碼段C和f4()的程式碼段D訪問的共享資源是resource2,它們沒有必要都競爭同一個同步鎖lock1。我們可以增加一個同步鎖lock2。f3()和f4()的程式碼可以修改為: 
public static final Object lock2 = new Object(); 

… f3() { 

synchronized(lock2){ // lock2 是公用同步鎖 
  // 程式碼段 C 
// 訪問共享資源 resource2 
// 需要同步 



… f4() { 

synchronized(lock2){ // lock2 是公用同步鎖 
  // 程式碼段 D 
// 訪問共享資源 resource2 
// 需要同步 



這樣,f1()和f2()就會競爭lock1,而f3()和f4()就會競爭lock2。這樣,分開來分別競爭兩個鎖,就可以大大較少同步鎖競爭的概率,從而減少系統的開銷。 

訊號量 

同步鎖模型只是最簡單的同步模型。同一時刻,只有一個執行緒能夠運行同步程式碼。 
有的時候,我們希望處理更加複雜的同步模型,比如生產者/消費者模型、讀寫同步模型等。這種情況下,同步鎖模型就不夠用了。我們需要一個新的模型。這就是我們要講述的訊號量模型。 
訊號量模型的工作方式如下:執行緒在執行的過程中,可以主動停下來,等待某個訊號量的通知;這時候,該執行緒就進入到該訊號量的待召(Waiting)隊列當中;等到通知之後,再繼續執行。 
很多語言裡面,同步鎖都由專門的物件表示,物件名通常叫Monitor。 
同樣,在很多語言中,訊號量通常也有專門的物件名來表示,比如,Mutex,Semphore。 
訊號量模型要比同步鎖模型複雜許多。一些系統中,訊號量甚至可以跨程序進行同步。另外一些訊號量甚至還有計數功能,能夠控制同時執行的執行緒數。 
我們沒有必要考慮那麼複雜的模型。所有那些複雜的模型,都是最基本的模型衍生出來的。只要掌握了最基本的訊號量模型——“等待/通知”模型,複雜模型也就迎刃而解了。 
我們還是以Java語言為例。Java語言裡面的同步鎖和訊號量概念都非常模糊,沒有專門的物件名詞來表示同步鎖和訊號量,只有兩個同步鎖相關的關鍵字——volatile和synchronized。 
這種模糊雖然導致概念不清,但同時也避免了Monitor、Mutex、Semphore等名詞帶來的種種誤解。我們不必執著於名詞之爭,可以專注於理解實際的執行原理。 
在Java語言裡面,任何一個Object Reference都可以作為同步鎖。同樣的道理,任何一個Object Reference也可以作為訊號量。 
Object物件的wait()方法就是等待通知,Object物件的notify()方法就是發出通知。 
具體呼叫方法為 
(1)等待某個訊號量的通知 
public static final Object signal = new Object(); 

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

    // 只有成功獲取了signal這個訊號量兼同步鎖之後,我們才可能進入這段程式碼 
    signal.wait(); // 這裡要放棄訊號量。本執行緒要進入signal訊號量的待召(Waiting)佇列 

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

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



需要注意的是,上述程式碼中的signal.wait()的意思。signal.wait()很容易導致誤解。signal.wait()的意思並不是說,signal開始wait,而是說,執行這段程式碼的當前執行緒開始wait這個signal物件,即進入signal物件的待召(Waiting)佇列。 

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

    // 只有成功獲取了signal這個訊號量兼同步鎖之後,我們才可能進入這段程式碼 
signal.notify(); // 這裡,我們通知signal的待召佇列中的某個執行緒。 

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



需要注意的是,signal.notify()的意思。signal.notify()並不是通知signal這個物件本身。而是通知正在等待signal訊號量的其他執行緒。 

以上就是Object的wait()和notify()的基本用法。 
實際上,wait()還可以定義等待時間,當執行緒在某訊號量的待召佇列中,等到足夠長的時間,就會等無可等,無需再等,自己就從待召佇列轉移到就緒佇列中了。 
另外,還有一個notifyAll()方法,表示通知待召佇列裡面的所有執行緒。 
這些細節問題,並不對大局產生影響。

----------------------------------------------------------------------------------------執行緒同步的總結4-------------------------------------------------------------------------

總的說來,synchronized關鍵字可以作為函式的修飾符,也可作為函式內的語句,也就是平時說的同步方法和同步語句塊。如果再細的分類,synchronized可作用於instance變數、object reference(物件引用)、static函式和class literals(類名稱字面常量)身上。

在進一步闡述之前,我們需要明確幾點:

A.無論synchronized關鍵字加在方法上還是物件上,它取得的鎖都是物件,而不是把一段程式碼或函式當作鎖――而且同步方法很可能還會被其他執行緒的物件訪問。

B.每個物件只有一個鎖(lock)與之相關聯。

C.實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

接著來討論synchronized用到不同地方對程式碼產生的影響:

假設P1、P2是同一個類的不同物件,這個類中定義了以下幾種情況的同步塊或同步方法,P1、P2就都可以呼叫它們。

1.  把synchronized當作函式修飾符時,示例程式碼如下:

Public synchronized void methodAAA()

{

//….

}

這也就是同步方法,那這時synchronized鎖定的是哪個物件呢?它鎖定的是呼叫這個同步方法物件。也就是說,當一個物件P1在不同的執行緒中執行這個同步方法時,它們之間會形成互斥,達到同步的效果。但是這個物件所屬的Class所產生的另一物件P2卻可以任意呼叫這個被加了synchronized關鍵字的方法。

上邊的示例程式碼等同於如下程式碼:

public void methodAAA()

{

synchronized (this)      //  (1)

{

       //…..

}

}

 (1)處的this指的是什麼呢?它指的就是呼叫這個方法的物件,如P1。可見同步方法實質是將synchronized作用於object reference。――那個拿到了P1物件鎖的執行緒,才可以呼叫P1的同步方法,而對P2而言,P1這個鎖與它毫不相干,程式也可能在這種情形下襬脫同步機制的控制,造成資料混亂:(

2.同步塊,示例程式碼如下:

            public void method3(SomeObject so)

              {

                     synchronized(so)

{

       //…..

}

}

這時,鎖就是so這個物件,誰拿到這個鎖誰就可以執行它所控制的那段程式碼。當有一個明確的物件作為鎖時,就可以這樣寫程式,但當沒有明確的物件作為鎖,只是想讓一段程式碼同步時,可以建立一個特殊的instance變數(它得是一個物件)來充當鎖:

class Foo implements Runnable

{

       private byte[] lock = new byte[0];  // 特殊的instance變數

    Public void methodA()

{

       synchronized(lock) { //… }

}

//…..

}

注:零長度的byte陣列物件建立起來將比任何物件都經濟――檢視編譯後的位元組碼:生成零長度的byte[]物件只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

3.將synchronized作用於static 函式,示例程式碼如下:

      Class Foo

{

public synchronized static void methodAAA()   // 同步的static 函式

{

//….

}

public void methodBBB()

{

       synchronized(Foo.class)   //  class literal(類名稱字面常量)

}

       }

   程式碼中的methodBBB()方法是把class literal作為鎖的情況,它和同步的static函式產生的效果是一樣的,取得的鎖很特別,是當前呼叫這個方法的物件所屬的類(Class,而不再是由這個Class產生的某個具體物件了)。

記得在《Effective Java》一書中看到過將 Foo.class和 P1.getClass()用於作同步鎖還不一樣,不能用P1.getClass()來達到鎖這個Class的目的。P1指的是由Foo類產生的物件。

可以推斷:如果一個類中定義了一個synchronized的static函式A,也定義了一個synchronized 的instance函式B,那麼這個類的同一物件Obj在多執行緒中分別訪問A和B兩個方法時,不會構成同步,因為它們的鎖都不一樣。A方法的鎖是Obj這個物件,而B的鎖是Obj所屬的那個Class。

小結如下:

搞清楚synchronized鎖定的是哪個物件,就能幫助我們設計更安全的多執行緒程式。

還有一些技巧可以讓我們對共享資源的同步訪問更加安全:

1.  定義private 的instance變數+它的 get方法,而不要定義public/protected的instance變數。如果將變數定義為public,物件在外界可以繞過同步方法的控制而直接取得它,並改動它。這也是JavaBean的標準實現方式之一。

2.  如果instance變數是一個物件,如陣列或ArrayList什麼的,那上述方法仍然不安全,因為當外界物件通過get方法拿到這個instance物件的引用後,又將其指向另一個物件,那麼這個private變數也就變了,豈不是很危險。 這個時候就需要將get方法也加上synchronized同步,並且,只返回這個private物件的clone()――這樣,呼叫端得到的就是物件副本的引用了。

相關推薦

Java——執行同步通訊

執行緒安全是指多個執行緒訪問同一程式碼,不會產生不確定的結果; 執行緒同步: 為了避免多執行緒在共享資源時發生衝突,所以 要線上程使用該資源時,就為執行緒上一把“鎖”,第一個訪問資源的執行緒為資源上鎖,其他執行緒若想訪問該資源,則必須等待解鎖為止,解鎖的同時,另

Java執行同步通訊

           當兩個或兩個以上的執行緒需要共享資源,它們需要某種方法來確定資源在某一刻僅被一個執行緒佔用。達到此目的的過程叫做同步(synchronization)。像你所看到的,Java為此

執行如何實現同步通訊

執行緒同步 什麼是執行緒同步? 當使用多個執行緒來訪問同一個資料時,非常容易出現執行緒安全問題(比如多個執行緒都在操作同一資料導致資料不一致),所以我們用同步機制來解決這些問題。 實現同步機制有兩個方法:1。同步程式碼塊:synchronized(同一個資料){} 同一個資

javaSE (三十五)多執行 ( 多執行實現方法區別、同步程式碼塊方法(執行安全))

主要還是熟悉api,熟悉方法,簡單,需要多實踐 1、 多執行緒實現方法和區別: 多執行緒實現的兩種方法: 1)類繼承Thread類或實現Runnable介面,重寫run()方法 2)建立Thread的子類物件(需要開幾個執行緒就建立幾個物件,可建立匿名內部類) 3)子類

執行同步非同步理解

//當個執行緒訪問同一個資源的時候,要注意執行緒同步的問題,如果不同步容易造成資料沒及時修改,然後就被另一個執行緒訪問,得到的資料還是上一次的資料,造成資料錯誤的情況,以下demo可以很容易發現,為了便於發現我在上面休眠100毫秒,如果將ticket設為方法內的區域性變數則就不會共享了。 pa

Python進階(二十六)-多執行實現同步的四種方式

分享一下我的偶像大神的人工智慧教程!http://blog.csdn.net/jiangjunshow 也歡迎轉載我的文章,轉載請註明出處 https://blog.csdn.net/mm2zzyzzp Python進階(二十六)-多執行緒實現同步的四種方式

執行實現udp網路通訊

本章節將介紹主執行緒與子執行緒的關係;使用udp利用多執行緒在python環境下實現全雙工通訊程式碼的兩種實現。(未完待續。。。) 一、主執行緒與子執行緒的關係: 1,若主執行緒無程式碼執行,主執行緒將等待子執行緒結束而結束。 2,執行緒的執行並無先後順序。 3,若主執行緒因特殊原因先結束,子執行緒也

Python 多執行、多程序 (二)之 多執行同步通訊

Python 多執行緒、多程序 (一)之 原始碼執行流程、GIL Python 多執行緒、多程序 (二)之 多執行緒、同步、通訊 Python 多執行緒、多程序 (三)之 執行緒程序對比、多執行緒 一、python多執行緒 對於I/O操作的時候,程序與執行緒的效能差別不大,甚至由於執行緒更輕量級,效能更高

JVM學習之java執行實現&排程狀態轉換

1 謹慎使用java 多執行緒   如何提升效率:      使用java時推薦利用多執行緒處理一些操作絕大多數情況下確實能提高效率,提高效率的原理在哪裡呢,為什麼說絕大多說情況呢。        在CPU單核時代,我們知道某一時刻工作的執行緒只能是一條,那多執行緒為什

【JavaSE】執行同步死鎖,synchronized物件鎖全域性鎖,一個生活的例子解釋。

1.多執行緒為什麼要加鎖? 因為在多執行緒啟動之後,所有執行緒都是無順序任意執行的,甚至幾乎同時訪問同一個資源或者程式碼塊,所以上一個執行緒對資源所做的改變,還沒來得及使用,就有可能被下一個執行緒所覆蓋。 引入鎖的概念,就是為了讓競爭資源在各個執行緒使用的時候

java socket通訊I/O阻塞>多執行實現非阻塞通訊

簡單的java socket通訊,多個客戶端同時連線,功能可在此基礎上進行擴充套件。效果如圖: server: package com.lb.LB_Socket; import java.io.BufferedReader; import ja

執行實現同步的七種方式

同步的方法: 一、同步方法   即有synchronized關鍵字修飾的方法。 由於java的每個物件都有一個內建鎖,當用此關鍵字修飾方法時, 內建鎖會保護整個方法。在呼叫該方法前,需要獲得內建鎖,否則就處於阻塞狀態。 注: synchronized關鍵字也可以修飾靜態方法,此時如果呼叫該靜態方法,將會鎖住整

Python 多執行、多程序 (二)之 多執行同步通訊

一、python多執行緒 對於I/O操作的時候,程序與執行緒的效能差別不大,甚至由於執行緒更輕量級,效能更高。這裡的I/O包括網路I/O和檔案I/O 1、例項 假如利用socket傳送http請求,也就是網路I/O。爬取列表網頁中的寫href連結,然後獲取href連結之後,在爬去連結的網頁詳情。 如果不適用

python3.4多執行實現同步的四種方式

臨界資源即那些一次只能被一個執行緒訪問的資源,典型例子就是印表機,它一次只能被一個程式用來執行列印功能,因為不能多個執行緒同時操作,而訪問這部分資源的程式碼通常稱之為臨界區。 1. 鎖機制 threading的Lock類,用該類的acquire函式進行加鎖,用real

三十九、Linux 執行——執行同步互斥

39.1 概念 執行緒同步 是一個巨集觀概念,在微觀上包含執行緒的相互排斥和執行緒先後執行的約束問題 解決同步方式 條件變數 執行緒訊號量 執行緒互斥 執行緒執行的相互排斥 解決互斥的方式

[150521]執行同步非同步

多執行緒和非同步操作的異同   多執行緒和非同步操作兩者都可以達到避免呼叫執行緒阻塞的目的,從而提高軟體的可響應性。甚至有些時候我們就認為多執行緒和非同步操作是等同的概念。但是,多執行緒和非同步操作還是有一些區別的。而這些區別造成了使用多執行緒和非同步操作的時機的區別

Linux執行淺析[執行同步互斥之執行互斥鎖]

Linux執行緒淺析[執行緒的同步和互斥] 執行緒同步 執行緒互斥 執行緒互斥的相關函式 執行緒同步的相關函式 執行緒同步 是巨集觀上的一個概念,在微觀上面包含執行緒的相互排斥和執行緒的執行順序的約束問題 解決方法: 條件變數 執行

關於多執行同步非同步的理解

執行緒同步:就是多個執行緒同時訪問同一資源,必須等一個執行緒訪問結束,才能訪問其它資源,比較浪費時間,效率低 執行緒非同步:訪問資源時在空閒等待時可以同時訪問其他資源,實現多執行緒機制 說起來比較抽象,我用程式碼嘗試了一下 //以非同步的方式提交佇列 -(

執行同步非同步區別

執行緒的同步是指一個執行緒需要等待上一個執行緒執行完成,才能執行當前執行緒,同步執行緒之間是相互制約的,在多執行緒中,同步機制是,多個執行緒同時訪問同一個資源,同一個時間內,只有一個執行緒可以擁有該資源的享用權,其他執行緒只能等待,這樣比較耗時、效率低。 例如

linux c 執行同步通訊)的幾種方法--互斥鎖,條件變數,訊號量,讀寫鎖

轉載自:https://blog.csdn.net/vertor11/article/details/55657619Linux下提供了多種方式來處理執行緒同步,最常用的是互斥鎖、條件變數、訊號量和讀寫鎖。 下面是思維導圖: 一、互斥鎖(mutex)   鎖機制是同一時刻只允