Java線程同步
當兩個或兩個以上的線程需要共享資源,它們需要某種方法來確定資源在某一刻僅被一個線程占用。達到此目的的過程叫做同步(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 中。構造函數也創(chuàng)建了一個調用該對象的run( )方法的新線程。該線程立即啟動。Caller類的run( )方法通過參數msg字符串調用Callme實例target的call( ) 方法。最后,Synch類由創(chuàng)建Callme的一個簡單實例和Caller的三個具有不同消息字符串的實例開始。
Callme的同一實例傳給每個Caller實例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// This program is not synchronized. class Callme { void call(String msg) { System.out.print( "[" + msg); try { Thread.sleep( 1000 ); } catch (InterruptedException e) { System.out.println( "Interrupted" ); } System.out.println( "]" ); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread( this ); t.start(); } public void run() { target.call(msg); } } class Synch { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello" ); Caller ob2 = new Caller(target, "Synchronized" ); Caller ob3 = new Caller(target, "World" ); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch (InterruptedException e) { System.out.println( "Interrupted" ); } } } |
該程序的輸出如下:
1
2
3
|
Hello[Synchronized[World] ] ] |
在本例中,通過調用sleep( ),call( )方法允許執(zhí)行轉換到另一個線程。該結果是三個消息字符串的混合輸出。該程序中,沒有阻止三個線程同時調用同一對象的同一方法的方法存在。這是一種競爭,因為三個線程爭著完成方法。例題用sleep( )使該影響重復和明顯。在大多數情況,競爭是更為復雜和不可預知的,因為你不能確定何時上下文轉換會發(fā)生。這使程序時而運行正常時而出錯。
為達到上例所想達到的目的,必須有權連續(xù)的使用call( )。也就是說,在某一時刻,必須限制只有一個線程可以支配它。為此,你只需在call( ) 定義前加上關鍵字synchronized,如下:
1
2
3
|
class Callme { synchronized void call(String msg) { ... |
這防止了在一個線程使用call( )時其他線程進入call( )。在synchronized加到call( )前面以后,程序輸出如下:
1
2
3
|
[Hello] [Synchronized] [World] |
任何時候在多線程情況下,你有一個方法或多個方法操縱對象的內部狀態(tài),都必須用synchronized 關鍵字來防止狀態(tài)出現競爭。記住,一旦線程進入實例的同步方法,沒有其他線程可以進入相同實例的同步方法。然而,該實例的其他不同步方法卻仍然可以被調用。
同步語句
盡管在創(chuàng)建的類的內部創(chuàng)建同步方法是獲得同步的簡單和有效的方法,但它并非在任何時候都有效。這其中的原因,請跟著思考。假設你想獲得不為多線程訪問設計的類對象的同步訪問,也就是,該類沒有用到synchronized方法。而且,該類不是你自己,而是第三方創(chuàng)建的,你不能獲得它的源代碼。這樣,你不能在相關方法前加synchronized修飾符。怎樣才能使該類的一個對象同步化呢?很幸運,解決方法很簡單:你只需將對這個類定義的方法的調用放入一個synchronized塊內就可以了。
下面是synchronized語句的普通形式:
1
2
3
|
synchronized (object) { // statements to be synchronized } |
其中,object是被同步對象的引用。如果你想要同步的只是一個語句,那么不需要花括號。一個同步塊確保對object成員方法的調用僅在當前線程成功進入object管程后發(fā)生。
下面是前面程序的修改版本,在run( )方法內用了同步塊:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// This program uses a synchronized block. class Callme { void call(String msg) { System.out.print( "[" + msg); try { Thread.sleep( 1000 ); } catch (InterruptedException e) { System.out.println( "Interrupted" ); } System.out.println( "]" ); } } class Caller implements Runnable { String msg; Callme target; Thread t; public Caller(Callme targ, String s) { target = targ; msg = s; t = new Thread( this ); t.start(); } // synchronize calls to call() public void run() { synchronized (target) { // synchronized block target.call(msg); } } } class Synch1 { public static void main(String args[]) { Callme target = new Callme(); Caller ob1 = new Caller(target, "Hello" ); Caller ob2 = new Caller(target, "Synchronized" ); Caller ob3 = new Caller(target, "World" ); // wait for threads to end try { ob1.t.join(); ob2.t.join(); ob3.t.join(); } catch (InterruptedException e) { System.out.println( "Interrupted" ); } } } |
這里,call( )方法沒有被synchronized修飾。而synchronized是在Caller類的run( )方法中聲明的。這可以得到上例中同樣正確的結果,因為每個線程運行前都等待先前的一個線程結束。
Java線程間通信
多線程通過把任務分成離散的和合乎邏輯的單元代替了事件循環(huán)程序。線程還有第二優(yōu)點:它遠離了輪詢。輪詢通常由重復監(jiān)測條件的循環(huán)實現。一旦條件成立,就要采取適當的行動。這浪費了CPU時間。舉例來說,考慮經典的序列問題,當一個線程正在產生數據而另一個程序正在消費它。為使問題變得更有趣,假設數據產生器必須等待消費者完成工作才能產生新的數據。在輪詢系統,消費者在等待生產者產生數據時會浪費很多CPU周期。一旦生產者完成工作,它將啟動輪詢,浪費更多的CPU時間等待消費者的工作結束,如此下去。很明顯,這種情形不受歡迎。
為避免輪詢,Java包含了通過wait( ),notify( )和notifyAll( )方法實現的一個進程間通信機制。這些方法在對象中是用final方法實現的,所以所有的類都含有它們。這三個方法僅在synchronized方法中才能被調用。盡管這些方法從計算機科學遠景方向上來說具有概念的高度先進性,實際中用起來是很簡單的:
wait( ) 告知被調用的線程放棄管程進入睡眠直到其他線程進入相同管程并且調用notify( )。
notify( ) 恢復相同對象中第一個調用 wait( ) 的線程。
notifyAll( ) 恢復相同對象中所有調用 wait( ) 的線程。具有最高優(yōu)先級的線程最先運行。
這些方法在Object中被聲明,如下所示:
1
2
3
|
final void wait( ) throws InterruptedException final void notify( ) final void notifyAll( ) |
wait( )存在的另外的形式允許你定義等待時間。
下面的例子程序錯誤的實行了一個簡單生產者/消費者的問題。它由四個類組成:Q,設法獲得同步的序列;Producer,產生排隊的線程對象;Consumer,消費序列的線程對象;以及PC,創(chuàng)建單個Q,Producer,和Consumer的小類。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// An incorrect implementation of a producer and consumer. class Q { int n; synchronized int get() { System.out.println( "Got: " + n); return n; } synchronized void put( int n) { this .n = n; System.out.println( "Put: " + n); } } class Producer implements Runnable { Q q; Producer(Q q) { this .q = q; new Thread( this , "Producer" ).start(); } public void run() { int i = 0 ; while ( true ) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this .q = q; new Thread( this , "Consumer" ).start(); } public void run() { while ( true ) { q.get(); } } } class PC { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println( "Press Control-C to stop." ); } } |
盡管Q類中的put( )和get( )方法是同步的,沒有東西阻止生產者超越消費者,也沒有東西阻止消費者消費同樣的序列兩次。這樣,你就得到下面的錯誤輸出(輸出將隨處理器速度和裝載的任務而改變):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Put: 1 Got: 1 Got: 1 Got: 1 Got: 1 Got: 1 Put: 2 Put: 3 Put: 4 Put: 5 Put: 6 Put: 7 Got: 7 |
生產者生成1后,消費者依次獲得同樣的1五次。生產者在繼續(xù)生成2到7,消費者沒有機會獲得它們。
用Java正確的編寫該程序是用wait( )和notify( )來對兩個方向進行標志,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
// A correct implementation of a producer and consumer. class Q { int n; boolean valueSet = false ; synchronized int get() { if (!valueSet) try { wait(); } catch (InterruptedException e) { System.out.println( "InterruptedException caught" ); } System.out.println( "Got: " + n); valueSet = false ; notify(); return n; } synchronized void put( int n) { if (valueSet) try { wait(); } catch (InterruptedException e) { System.out.println( "InterruptedException caught" ); } this .n = n; valueSet = true ; System.out.println( "Put: " + n); notify(); } } class Producer implements Runnable { Q q; Producer(Q q) { this .q = q; new Thread( this , "Producer" ).start(); } public void run() { int i = 0 ; while ( true ) { q.put(i++); } } } class Consumer implements Runnable { Q q; Consumer(Q q) { this .q = q; new Thread( this , "Consumer" ).start(); } public void run() { while ( true ) { q.get(); } } } class PCFixed { public static void main(String args[]) { Q q = new Q(); new Producer(q); new Consumer(q); System.out.println( "Press Control-C to stop." ); } } |
內部get( ), wait( )被調用。這使執(zhí)行掛起直到Producer 告知數據已經預備好。這時,內部get( ) 被恢復執(zhí)行。獲取數據后,get( )調用notify( )。這告訴Producer可以向序列中輸入更多數據。在put( )內,wait( )掛起執(zhí)行直到Consumer取走了序列中的項目。當執(zhí)行再繼續(xù),下一個數據項目被放入序列,notify( )被調用,這通知Consumer它應該移走該數據。
下面是該程序的輸出,它清楚的顯示了同步行為:
1
2
3
4
5
6
7
8
9
10
|
Put: 1 Got: 1 Put: 2 Got: 2 Put: 3 Got: 3 Put: 4 Got: 4 Put: 5 Got: 5 |