標籤:
當兩個或兩個以上的線程需要共用資源,它們需要某種方法來確定資源在某一刻僅被一個線程佔用。達到此目的的過程叫做同步(synchronization)。像你所看到的,Java為此提供了獨特的,語言水平上的支援。
同步的關鍵是管程(也叫訊號量semaphore)的概念。管程是一個互斥獨佔鎖定的對象,或稱互斥體(mutex)。在給定的時間,僅有一個線程可以獲得管程。當一個線程需要鎖定,它必須進入管程。所有其他的試圖進入已經鎖定的管程的線程必須掛起直到第一個線程退出管程。這些其他的線程被稱為等待管程。一個擁有管程的線程如果願意的話可以再次進入相同的管程。
如果你用其他語言例如C或C++時用到過同步,你會知道它用起來有一點詭異。這是因為很多語言它們自己不支援同步。相反,對同步線程,程式必須利用作業系統源語。幸運的是Java通過語言元素實現同步,大多數的與同步相關的複雜性都被消除。
你可以用兩種方法同步化代碼。兩者都包括synchronized關鍵字的運用,下面分別說明這兩種方法。
使用同步方法
Java中同步是簡單的,因為所有對象都有它們與之對應的隱式管程。進入某一對象的管程,就是調用被synchronized關鍵字修飾的方法。當一個線程在一個同步方法內部,所有試圖調用該方法(或其他同步方法)的同執行個體的其他線程必須等待。為了退出管程,並放棄對對象的控制權給其他等待的線程,擁有管程的線程僅需從同步方法中返回。
為理解同步的必要性,讓我們從一個應該使用同步卻沒有用的簡單例子開始。下面的程式有三個簡單類。首先是Callme,它有一個簡單的方法call( )。call( )方法有一個名為msg的String參數。該方法試圖在方括弧內列印msg 字串。有趣的事是在調用call( ) 列印左括弧和msg字串後,調用Thread.sleep(1000),該方法使當前線程暫停1秒。
下一個類的建構函式Caller,引用了Callme的一個執行個體以及一個String,它們被分別存在target 和 msg 中。建構函式也建立了一個調用該對象的run( )方法的新線程。該線程立即啟動。Caller類的run( )方法通過參數msg字串調用Callme執行個體target的call( ) 方法。最後,Synch類由建立Callme的一個簡單一實例和Caller的三個具有不同訊息字串的執行個體開始。
Callme的同一執行個體傳給每個Caller執行個體。
1 // This program is not synchronized. 2 class Callme { 3 void call(String msg) { 4 System.out.print("[" + msg); 5 try { 6 Thread.sleep(1000); 7 } catch(InterruptedException e) { 8 System.out.println("Interrupted"); 9 }10 System.out.println("]");11 }12 }13 14 class Caller implements Runnable {15 String msg;16 Callme target;17 Thread t;18 public Caller(Callme targ, String s) {19 target = targ;20 msg = s;21 t = new Thread(this);22 t.start();23 }24 public void run() {25 target.call(msg);26 }27 }28 29 class Synch {30 public static void main(String args[]) {31 Callme target = new Callme();32 Caller ob1 = new Caller(target, "Hello");33 Caller ob2 = new Caller(target, "Synchronized");34 Caller ob3 = new Caller(target, "World");35 // wait for threads to end36 try {37 ob1.t.join();38 ob2.t.join();39 ob3.t.join();40 } catch(InterruptedException e) {41 System.out.println("Interrupted");42 }43 }44 }
該程式的輸出如下:
Hello[Synchronized[World]
]
]
在本例中,通過調用sleep( ),call( )方法允許執行轉換到另一個線程。該結果是三個訊息字串的混合輸出。該程式中,沒有阻止三個線程同時調用同一對象的同一方法的方法存在。這是一種競爭,因為三個線程爭著完成方法。例題用sleep( )使該影響重複和明顯。在大多數情況,競爭是更為複雜和不可預知的,因為你不能確定何時上下文轉換會發生。這使程式時而運行正常時而出錯。
為達到上例所想達到的目的,必須有權連續的使用call( )。也就是說,在某一時刻,必須限制只有一個線程可以支配它。為此,你只需在call( ) 定義前加上關鍵字synchronized,如下:
1 class Callme {2 synchronized void call(String msg) {3 ...
這防止了在一個線程使用call( )時其他線程進入call( )。在synchronized加到call( )前面以後,程式輸出如下:
[Hello]
[Synchronized]
[World]
任何時候在多線程情況下,你有一個方法或多個方法操縱對象的內部狀態,都必須用synchronized 關鍵字來防止狀態出現競爭。記住,一旦線程進入執行個體的同步方法,沒有其他線程可以進入相同執行個體的同步方法。然而,該執行個體的其他不同步方法卻仍然可以被調用。
同步語句
儘管在建立的類的內部建立同步方法是獲得同步的簡單和有效方法,但它並非在任何時候都有效。這其中的原因,請跟著思考。假設你想獲得不為多線程訪問設計的類對象的同步訪問,也就是,該類沒有用到synchronized方法。而且,該類不是你自己,而是第三方建立的,你不能獲得它的原始碼。這樣,你不能在相關方法前加synchronized修飾符。怎樣才能使該類的一個對象同步化呢?很幸運,解決方案很簡單:你只需將對這個類定義的方法的調用放入一個synchronized塊內就可以了。
下面是synchronized語句的普通形式:
1 synchronized(object) {2 // statements to be synchronized3 }
其中,object是被同步對象的引用。如果你想要同步的只是一個語句,那麼不需要花括弧。一個同步塊確保對object成員方法的調用僅在當前線程成功進入object管程後發生。
下面是前面程式的修改版本,在run( )方法內用了同步塊:
1 // This program uses a synchronized block. 2 class Callme { 3 void call(String msg) { 4 System.out.print("[" + msg); 5 try { 6 Thread.sleep(1000); 7 } catch (InterruptedException e) { 8 System.out.println("Interrupted"); 9 }10 System.out.println("]");11 }12 }13 14 class Caller implements Runnable {15 String msg;16 Callme target;17 Thread t;18 public Caller(Callme targ, String s) {19 target = targ;20 msg = s;21 t = new Thread(this);22 t.start();23 }24 25 // synchronize calls to call()26 public void run() {27 synchronized(target) { // synchronized block28 target.call(msg);29 }30 }31 }32 33 class Synch1 {34 public static void main(String args[]) {35 Callme target = new Callme();36 Caller ob1 = new Caller(target, "Hello");37 Caller ob2 = new Caller(target, "Synchronized");38 Caller ob3 = new Caller(target, "World");39 40 // wait for threads to end41 try {42 ob1.t.join();43 ob2.t.join();44 ob3.t.join();45 } catch(InterruptedException e) {46 System.out.println("Interrupted");47 }48 }49 }
這裡,call( )方法沒有被synchronized修飾。而synchronized是在Caller類的run( )方法中聲明的。這可以得到上例中同樣正確的結果,因為每個線程運行前都等待先前的一個線程結束。
系列文章:
Java知多少(上)
Java知多少(39)interface介面
Java知多少(40)介面和抽象類別的區別
Java知多少(41)泛型詳解
Java知多少(42)泛型萬用字元和型別參數的範圍
Java知多少(43)異常處理基礎
Java知多少(44)異常類型
Java知多少(45)未被捕獲的異常
Java知多少(46)try和catch的使用
Java知多少(47)多重catch語句的使用
Java知多少(48)try語句的嵌套
Java知多少(49)throw:異常的拋出
Java知多少(50)Java throws子句
Java知多少(51)finally
Java知多少(52)內建異常
Java知多少(53)使用Java建立自己的異常子類
Java知多少(54)斷言詳解
Java知多少(55)線程
Java知多少(56)執行緒模式Java知多少(57)主線程Java知多少(58)線程Runnable介面和Thread類詳解Java知多少(59)建立多線程Java知多少(60)isAlive()和join()的使用Java知多少(61)線程優先順序
Java知多少(62)線程同步