鎖同步程式碼塊
鎖同步程式碼塊
Java還提供一種同步程式碼塊的機制,基於Lock(屬於java.util.concurrent.locks包)介面實現(像ReentrantLock),它比synchronized關鍵字更強大且靈活。這種機制的優勢描述如下:
- 更靈活的方式構造同步程式碼塊。使用sychronized關鍵字只能以一種結構方式來控制同步程式碼塊,但是,Lock介面允許更加複雜的結構來實現臨界區。
- 相比synchronized關鍵字,Lock介面還提供附加的功能。其中一個新功能是通過tryLock()方法實現,這個方法嘗試去控制鎖,如果此方法被其它執行緒使用而無法控制的話,返回false。使用synchronized關鍵字,如果執行緒A嘗試執行一段執行緒B正在執行的同步程式碼塊,執行緒A將暫停直到執行緒B結束同步塊的執行。使用鎖機制,即可執行tryLock()方法,這個方法在判斷是否有其它執行緒正在執行被鎖保護的程式碼時返回布林值。
- ReadWriteLock介面允許多個訪問者和一個修改者進行讀寫分離操作。
- Lock介面效能優於synchronized關鍵字。
ReentrantLock類的建構函式包含一個名為fair的boolean型引數,用來控制其行為。此引數預設值為false,稱為非公允模式,在此模式下,如果一些執行緒在等待需要選擇其中一個執行緒來訪問臨界區的鎖時,它會隨機選擇一個執行緒。引數值為true時稱為公允模式,在此模式下,如果一些執行緒在等待需要選擇其中一個執行緒來訪問臨界區的鎖時,它將選擇等待時間最長的執行緒。考慮到之前解釋的特性只用到lock()和unlock()方法,因為tryLock()方法在Lock介面被使用時不會讓執行緒休眠,公允屬性就不會影響到此方法的功能。
在本節中,學習如何使用鎖來同步程式碼塊,以及使用ReentrantLock類和其實現的Lock介面建立臨界區,來模擬列印佇列。還會學習公允引數如何影響Lock的行為。
準備工作
本範例通過Eclipse開發工具實現。如果使用諸如NetBeans的開發工具,開啟並建立一個新的Java專案。
實現過程
通過如下步驟完成範例:
-
建立名為PrintQueue的類,用來實現列印佇列:
public class PrintQueue {
-
定義Lock物件,在建構函式中用ReentrantLock類的新物件進行初始化。建構函式會接收一個Boolean引數,此引數將用於指定Lock的公允模式:
private Lock queueLock; public PrintQueue(boolean fairMode){ queueLock = new ReentrantLock(fairMode); }
-
實現printJob()方法,接收Object為引數且不會返回任何值:
public void printJob(Object document){
-
在printJob()方法內,呼叫lock()方法控制Lock物件:
queueLock.lock();
-
然後,包括如下程式碼來模擬列印一個檔案的流程:
try { Long duration = (long)(Math.random() * 10000); System.out.println(Thread.currentThread().getName()+ ": PrintQueue: Printing the first Job during "+(duration/1000)+" seconds"); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); }
-
最後使用unlock()方法取消對Lock物件的控制:
finally{ queueLock.unlock(); }
-
然後,重複執行列印流程。printJob()方法將兩次使用和釋放鎖。這種詭異的操作行為以一種更好的方式展現公允模式與非公允模式的區別。在printJob()方法中加入如下程式碼:
queueLock.lock(); try { Long duration = (long)(Math.random() * 10000); System.out.printf("%s: PrintQueue: Printing the second Job during %d seconds\n", Thread.currentThread().getName(), (duration/1000)); Thread.sleep(duration); } catch (InterruptedException e) { e.printStackTrace(); } finally{ queueLock.unlock(); }
-
建立名為Job的類,指定其實現Runnable介面:
public class Job implements Runnable {
-
定義PrintQueue類的物件,實現初始化此物件的類建構函式:
private PrintQueue printQueue; public Job(PrintQueue printQueue){ this.printQueue = printQueue; }
-
實現run()方法,使用PrintQueue物件傳送列印操作:
@Override public void run() { System.out.printf("%s: Going to print a document\n", Thread.currentThread().getName()); printQueue.printJob(new Object()); System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName()); }
-
建立本範例中的主類,實現一個包含main()方法的Main類:
public class Main { public static void main(String[] args) {
-
使用一個鎖測試PrintQueue類,此鎖的公允模式分別返回true和false。我們使用一個輔助方法來實現兩個測試,以便於main()方法的程式碼簡單化:
System.out.printf("Running example with fair-mode = false\n"); testPrintQueue(false); System.out.printf("Running example with fair-mode = true\n"); testPrintQueue(true); }
-
建立輔助方法testPrintQueue(),在方法內建立一個共享的PrintQueue物件:
private static void testPrintQueue(Boolean fairMode) { PrintQueue printQueue = new PrintQueue(fairMode);
-
建立10個Job物件以及10個執行物件的執行緒:
Thread thread[] = new Thread[10]; for (int i = 0 ; i < 10 ; i++){ thread[i] = new Thread(new Job(printQueue), "Thread "+ i); }
-
執行這10個執行緒:
for (int i = 0 ; i < 10 ; i++){ thread[i].start(); }
-
最後,等待這10個執行緒執行結束:
for (int i = 0 ; i < 10 ; i++){ try { thread[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
工作原理
下圖顯示執行本範例輸出的部分內容:
範例的關鍵之處在PrintQueue類的printJob()方法中。當想要使用鎖建立臨界區以及確保只有一個執行執行緒執行一段程式碼塊時,就必須建立ReentrantLock物件。在臨界區初始階段,需要使用lock()方法控制鎖。當執行緒A呼叫lock()方法時,如果沒有執行緒控制這個鎖,它允許執行緒A控制鎖並且立即返回,以便執行緒A進入臨界區執行。否則,如果執行緒B正在被鎖控制的臨界區裡執行,lock()方法則讓執行緒A休眠直到執行緒B在臨界區裡執行結束。
在臨界區結尾,使用unlock()方法釋放鎖控制,並允許其它執行緒進入臨界區執行。如果在臨界區結尾不呼叫unlock()方法的話,其它等待執行的執行緒將會一直等待下去,導致死鎖局面。如果在臨界區中使用try-catch程式塊,切記在finally部分里加入unlock()方法。
範例中測試的另一個特性時公允模式。每次列印操作中有兩個臨界區。如上圖所示,會看到所有操作中,第二部分緊隨第一個執行。這是正常情況,但非公允模式發生時就會有異常,也就是說,給ReentrantLock類建構函式傳false值。
與之相反,當通過給Lock類建構函式傳遞true值建立公允模式時,就具有不同的行為。第一個請求控制鎖的執行緒是Thread0,然後是Thread1,以此類推。當Thread0正在執行被鎖保護的第一個程式碼塊時,還有九個執行緒等待執行同一個程式碼塊。當Thread0釋放鎖時,它會立刻再次請求控制鎖,所以就是有10個執行緒同時嘗試控制鎖。當公允模式生效後,Lock介面將選擇Thread1,因為它已經等待更多的時間。然後,Lock介面選擇Thread2,然後Thread3,以此類推。在所有的執行緒通過鎖保護的第一個程式碼塊之前,沒有執行緒去執行鎖保護的第二個程式碼塊。一旦所有執行緒已經執行完鎖保護的第一個程式碼塊,然後重新排隊,Thread0,Thread1,以此類推。如下圖所示:
擴充套件學習
tryLock()方法,是Lock介面(ReentrantLock類)中另一個控制鎖的方法。與lock()方法最大的不同是,如果使用此方法的執行緒無法得到Lock介面的控制,tryLock()會立即返回並且不會讓執行緒休眠。如果執行緒控制鎖的話則返回boolean型別值true,否則返回false。也可以傳遞時間值和TimeUnit物件來指明執行緒等待鎖的最長持續時間。如果時間過去後執行緒依然沒有得到鎖,tryLock()方法將返回false值。TimeUnit是一個列舉型別的類,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、SECONDS,表明傳遞給方法的時間單位。
考慮到開發人員的職責是關注方法結果以及相應地表現。如果tryLock()方法返回false,很顯然程式無法在臨界區裡執行。即便執行通過,程式也可能得到錯誤的結果。
ReentrantLock類允許遞迴呼叫。當一個執行緒控制鎖並且進行一次遞迴呼叫時,它將繼續控制這個鎖,所以呼叫的lock()方法將立即返回,而執行緒將繼續執行遞迴呼叫。此外,也可以呼叫其它方法。在程式碼中,呼叫unlock()方法的次數與呼叫lock()方法的次數相同。
避免死鎖
為了避免死鎖,需要非常小心的使用鎖機制。當兩個或多個執行緒同時等待鎖時被阻塞的話,這種情況會導致永遠不會解鎖。例如,執行緒A控制了鎖X,執行緒B控制了鎖Y。如果執行緒A嘗試控制鎖Y,同時執行緒B嘗試控制鎖X,兩個執行緒將被無限期的阻塞,因為它們都在等待永遠不會被釋放的鎖。切記這種問題發生是因為兩個執行緒嘗試逆序控制鎖。第十一章“併發程式設計設計”的目錄提供一些好的建議來設計合適的併發應用,同時避免死鎖問題。
更多關注
- 本章中”同步方法“和“同步程式中使用狀態”小節。
- 第九章“測試併發應用”中的“監控鎖介面”小節。