Java併發編程指南(二):基本線程同步

1. 同步方法:

synchronized關鍵字:
只有一個執行線程將會訪問一個對象中被synchronized關鍵字聲明的方法。如果另一個線程試圖訪問同一個對象中任何被synchronized關鍵字聲明的方法,它將被暫停,直到第一個線程結束方法的執行。換句話說,每個方法聲明爲synchronized關鍵字是一個臨界區,Java只允許一個對象執行其中的一個臨界區。

靜態方法有不同的行爲。只有一個執行線程訪問被synchronized關鍵字聲明的靜態方法,但另一個線程可以訪問該類的一個對象中的其他非靜態的方法。 你必須非常小心這一點,因爲兩個線程可以訪問兩個不同的同步方法,如果其中一個是靜態的而另一個不是。如果這兩種方法改變相同的數據,你將會有數據不一致 的錯誤。

synchronized關鍵字不利於應用程序的性能,所以你必須僅在修改共享數據的併發環境下的方法上使用它。你可以使用遞歸調用synchronized方法。當線程訪問一個對象的synchronized方法,你可以遞歸調用該對象的其他synchronized方法,包括正在執行的方法。它將不需要再次獲取訪問synchronized方法的鎖。

我們可以使用synchronized關鍵字來保護訪問的代碼塊,替換在整個方法上使用synchronized關鍵字。我們應該使用 synchronized關鍵字以這樣的方式來保護訪問的共享數據,其餘的操作留出此代碼塊,這將會獲得更好的應用程序性能。這個目標就是讓臨界區(在同 一時刻可以被多個線程訪問的代碼塊)儘可能短。我們已經使用了synchronized關鍵字來保護訪問指令,將不使用共享數據的長操作留出此代碼塊。當 你以這個方式使用synchronized關鍵字,你必須通過一個對象引用作爲參數。只有一個線程可以訪問那個對象的synchronized代碼(代碼 塊或方法)。通常,我們將使用this關鍵字引用執行該方法的對象。

synchronized (this) {
// Java code
}
2. 在同步的類裏安排獨立屬性:

當你使用synchronized關鍵字來保護代碼塊,你使用一個對象作爲參數。JVM可以保證只有一個線程可以訪問那個對象保護所有的代碼塊(請注意,我們總是談論的對象,而不是類)。

3. 在同步代碼中使用條件:

在併發編程中的一個經典問題是生產者與消費者問題,我們有一個數據緩衝區,一個或多個數據的生產者在緩衝區存儲數據,而一個或多個數據的消費者,把數據從緩衝區取出。
由於緩衝區是一個共享的數據結構,我們必須採用同步機制,比如synchronized關鍵字來控制對它的訪問。但是我們有更多的限制因素,如果緩衝區是滿的,生產者不能存儲數據,如果緩衝區是空的,消費者不能取出數據。
對於這些類型的情況,Java在Object對象中提供wait(),notify(),notifyAll() 方法的實現。一個線程可以在synchronized代碼塊中調用wait()方法。如果在synchronized代碼塊外部調用wait()方法,JVM會拋出IllegalMonitorStateException異常。當線程調用wait()方法,JVM讓這個線程睡眠,並且釋放控制 synchronized代碼塊的對象,這樣,雖然它正在執行但允許其他線程執行由該對象保護的其他synchronized代碼塊。爲了喚醒線程,你必 須在由相同對象保護的synchronized代碼塊中調用notify()或notifyAll()方法。

4. 使用Lock同步代碼塊:

Java提供另外的機制用來同步代碼塊。它比synchronized關鍵字更加強大、靈活。它是基於Lock接口和實現它的類(如ReentrantLock)。這種機制有如下優勢:

  • 它允許以一種更靈活的方式來構建synchronized塊。使用synchronized關鍵字,你必須以結構化方式得到釋放synchronized代碼塊的控制權。Lock接口允許你獲得更復雜的結構來實現你的臨界區。
  • Lock 接口比synchronized關鍵字提供更多額外的功能。新功能之一是實現的tryLock()方法。這種方法試圖獲取鎖的控制權並且如果它不能獲取該鎖,是因爲其他線程在使用這個鎖,它將返回false。使用synchronized關鍵字,當線程A試圖執行synchronized代碼塊,如果線程B正在執行它,那麼線程A將阻塞直到線程B執行完synchronized代碼塊。使用鎖,你可以執行tryLock()方法,這個方法返回一個 Boolean值表示是否有其他線程正在運行這個鎖所保護的代碼。
  • 當有多個讀者和一個寫者時,Lock接口允許讀寫操作分離。
  • Lock接口比synchronized關鍵字提供更好的性能。

Lock 接口(和ReentrantLock類)包含其他方法來獲取鎖的控制權,那就是tryLock()方法。這個方法與lock()方法的最大區別是,如果一 個線程調用這個方法不能獲取Lock接口的控制權時,將會立即返回並且不會使這個線程進入睡眠。這個方法返回一個boolean值,true表示這個線程 獲取了鎖的控制權,false則表示沒有。

註釋:考慮到這個方法的結果,並採取相應的措施,這是程序員的責任。如果這個方法返回false值,預計你的程序不會執行這個臨界區。如果是這樣,你可能會在你的應用程序中得到錯誤的結果。

ReentrantLock類也允許遞歸調用(鎖的可重入性,譯者注),當一個線程有鎖的控制權並且使用遞歸調用,它延續了鎖的控制權,所以調用lock()方法將會立即返回並且繼續遞歸調用的執行。此外,我們也可以調用其他方法。

你必須要非常小心使用鎖來避免死鎖,這種情況發生在,當兩個或兩個以上的線程被阻塞等待將永遠不會解開的鎖。比如,線程A鎖定Lock(X)而線程B鎖定 Lock(Y)。如果現在,線程A試圖鎖住Lock(Y)而線程B同時也試圖鎖住Lock(X),這兩個線程將無限期地被阻塞,因爲它們等待的鎖將不會被解開。請注意,這個問題的發生是因爲這兩個線程嘗試以相反的順序獲取鎖(譯者注:鎖順序死鎖)。

5. 使用讀/寫鎖同步數據訪問:
鎖所提供的最重要的改進之一就是ReadWriteLock接口和唯一 一個實現它的ReentrantReadWriteLock類。這個類提供兩把鎖,一把用於讀操作和一把用於寫操作。同時可以有多個線程執行讀操作,但只有一個線程可以執行寫操作。當一個線程正在執行一個寫操作,不可能有任何線程執行讀操作。

用於讀操作的鎖是通過在ReadWriteLock接口中聲明的readLock()方法獲取的。這個鎖是實現Lock接口的一個對象,所以我們可以使用lock(), unlock() 和tryLock()方法。用於寫操作的鎖,是通過在ReadWriteLock接口中聲明的writeLock()方法獲取的。這個鎖是實現Lock接 口的一個對象,所以我們可以使用lock(), unlock() 和tryLock()方法。確保正確的使用這些鎖,使用它們與被設計的目的是一樣的,這是程序猿的職責。當你獲得Lock接口的讀鎖時,不能修改這個變量的值。否則,你可能會有數據不一致的錯誤。

6. 修改Lock的公平性:

ReentrantLock類和 ReentrantReadWriteLock類的構造器中,允許一個名爲fair的boolean類型參數,它允許你來控制這些類的行爲。默認值爲 false,這將啓用非公平模式。在這個模式中,當有多個線程正在等待一把鎖(ReentrantLock或者 ReentrantReadWriteLock),這個鎖必須選擇它們中間的一個來獲得進入臨界區,選擇任意一個是沒有任何標準的。true值將開啓公平 模式。在這個模式中,當有多個線程正在等待一把鎖(ReentrantLock或者ReentrantReadWriteLock),這個鎖必須選擇它們 中間的一個來獲得進入臨界區,它將選擇等待時間最長的線程。考慮到之前解釋的行爲只是使用lock()和unlock()方法。由於tryLock()方 法並不會使線程進入睡眠,即使Lock接口正在被使用,這個公平屬性並不會影響它的功能。

7. 在Lock中使用多個條件:

一個鎖可能伴隨着多個條件。這些條件聲明在Condition接口中。 這些條件的目的是允許線程擁有鎖的控制並且檢查條件是否爲true,如果是false,那麼線程將被阻塞,直到其他線程喚醒它們。Condition接口提供一種機制,阻塞一個線程和喚醒一個被阻塞的線程。

所 有Condition對象都與鎖有關,並且使用聲明在Lock接口中的newCondition()方法來創建。使用condition做任何操作之前, 你必須獲取與這個condition相關的鎖的控制。所以,condition的操作一定是在以調用Lock對象的lock()方法爲開頭,以調用相同 Lock對象的unlock()方法爲結尾的代碼塊中。

當一個線程在一個condition上調用await()方法時,它將自動釋放鎖的控制,所以其他線程可以獲取這個鎖的控制並開始執行相同操作,或者由同個鎖保護的其他臨界區。

註釋:當一個線程在一個condition上調用signal()或signallAll()方法,一個或者全部在這個condition上等待的線程將被喚醒。這並不能保證的使它們現在睡眠的條件現在是true,所以你必須在while循環內部調用await()方法。你不能離開這個循環,直到 condition爲true。當condition爲false,你必須再次調用 await()方法。
你必須十分小心 ,在使用await()和signal()方法時。如果你在condition上調用await()方法而卻沒有在這個condition上調用signal()方法,這個線程將永遠睡眠下去。

在調用await()方法後,一個線程可以被中斷的,所以當它正在睡眠時,你必須處理InterruptedException異常。

Condition接口提供不同版本的await()方法,如下:

  • await(long time, TimeUnit unit):這個線程將會一直睡眠直到:

(1)它被中斷

(2)其他線程在這個condition上調用singal()或signalAll()方法

(3)指定的時間已經過了

(4)TimeUnit類是一個枚舉類型如下的常量:

DAYS,HOURS, MICROSECONDS, MILLISECONDS, MINUTES, NANOSECONDS,SECONDS

  • awaitUninterruptibly():這個線程將不會被中斷,一直睡眠直到其他線程調用signal()或signalAll()方法
  • awaitUntil(Date date):這個線程將會一直睡眠直到:

(1)它被中斷

(2)其他線程在這個condition上調用singal()或signalAll()方法

(3)指定的日期已經到了

你可以在一個讀/寫鎖中的ReadLock和WriteLock上使用conditions。




參考資料:《Java 7 Concurrency Cookbook》

                 《Java 9 Concurrency Cookbook Second Edition》


發佈了70 篇原創文章 · 獲贊 28 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章