Java線(xiàn)程同步
當(dāng)兩個(gè)或兩個(gè)以上的線(xiàn)程需要共享資源,它們需要某種方法來(lái)確定資源在某一刻僅被一個(gè)線(xiàn)程占用。達(dá)到此目的的過(guò)程叫做同步(synchronization)。像你所看到的,Java為此提供了獨(dú)特的,語(yǔ)言水平上的支持。
同步的關(guān)鍵是管程(也叫信號(hào)量semaphore)的概念。管程是一個(gè)互斥獨(dú)占鎖定的對(duì)象,或稱(chēng)互斥體(mutex)。在給定的時(shí)間,僅有一個(gè)線(xiàn)程可以獲得管程。當(dāng)一個(gè)線(xiàn)程需要鎖定,它必須進(jìn)入管程。所有其他的試圖進(jìn)入已經(jīng)鎖定的管程的線(xiàn)程必須掛起直到第一個(gè)線(xiàn)程退出管程。這些其他的線(xiàn)程被稱(chēng)為等待管程。一個(gè)擁有管程的線(xiàn)程如果愿意的話(huà)可以再次進(jìn)入相同的管程。
如果你用其他語(yǔ)言例如C或C++時(shí)用到過(guò)同步,你會(huì)知道它用起來(lái)有一點(diǎn)詭異。這是因?yàn)楹芏嗾Z(yǔ)言它們自己不支持同步。相反,對(duì)同步線(xiàn)程,程序必須利用操作系統(tǒng)源語(yǔ)。幸運(yùn)的是Java通過(guò)語(yǔ)言元素實(shí)現(xiàn)同步,大多數(shù)的與同步相關(guān)的復(fù)雜性都被消除。
你可以用兩種方法同步化代碼。兩者都包括synchronized關(guān)鍵字的運(yùn)用,下面分別說(shuō)明這兩種方法。
使用同步方法
Java中同步是簡(jiǎn)單的,因?yàn)樗袑?duì)象都有它們與之對(duì)應(yīng)的隱式管程。進(jìn)入某一對(duì)象的管程,就是調(diào)用被synchronized關(guān)鍵字修飾的方法。當(dāng)一個(gè)線(xiàn)程在一個(gè)同步方法內(nèi)部,所有試圖調(diào)用該方法(或其他同步方法)的同實(shí)例的其他線(xiàn)程必須等待。為了退出管程,并放棄對(duì)對(duì)象的控制權(quán)給其他等待的線(xiàn)程,擁有管程的線(xiàn)程僅需從同步方法中返回。
為理解同步的必要性,讓我們從一個(gè)應(yīng)該使用同步卻沒(méi)有用的簡(jiǎn)單例子開(kāi)始。下面的程序有三個(gè)簡(jiǎn)單類(lèi)。首先是Callme,它有一個(gè)簡(jiǎn)單的方法call( )。call( )方法有一個(gè)名為msg的String參數(shù)。該方法試圖在方括號(hào)內(nèi)打印msg 字符串。有趣的事是在調(diào)用call( ) 打印左括號(hào)和msg字符串后,調(diào)用Thread.sleep(1000),該方法使當(dāng)前線(xiàn)程暫停1秒。
下一個(gè)類(lèi)的構(gòu)造函數(shù)Caller,引用了Callme的一個(gè)實(shí)例以及一個(gè)String,它們被分別存在target 和 msg 中。構(gòu)造函數(shù)也創(chuàng)建了一個(gè)調(diào)用該對(duì)象的run( )方法的新線(xiàn)程。該線(xiàn)程立即啟動(dòng)。Caller類(lèi)的run( )方法通過(guò)參數(shù)msg字符串調(diào)用Callme實(shí)例target的call( ) 方法。最后,Synch類(lèi)由創(chuàng)建Callme的一個(gè)簡(jiǎn)單實(shí)例和Caller的三個(gè)具有不同消息字符串的實(shí)例開(kāi)始。
Callme的同一實(shí)例傳給每個(gè)Caller實(shí)例。
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] ] ] |
在本例中,通過(guò)調(diào)用sleep( ),call( )方法允許執(zhí)行轉(zhuǎn)換到另一個(gè)線(xiàn)程。該結(jié)果是三個(gè)消息字符串的混合輸出。該程序中,沒(méi)有阻止三個(gè)線(xiàn)程同時(shí)調(diào)用同一對(duì)象的同一方法的方法存在。這是一種競(jìng)爭(zhēng),因?yàn)槿齻€(gè)線(xiàn)程爭(zhēng)著完成方法。例題用sleep( )使該影響重復(fù)和明顯。在大多數(shù)情況,競(jìng)爭(zhēng)是更為復(fù)雜和不可預(yù)知的,因?yàn)槟悴荒艽_定何時(shí)上下文轉(zhuǎn)換會(huì)發(fā)生。這使程序時(shí)而運(yùn)行正常時(shí)而出錯(cuò)。
為達(dá)到上例所想達(dá)到的目的,必須有權(quán)連續(xù)的使用call( )。也就是說(shuō),在某一時(shí)刻,必須限制只有一個(gè)線(xiàn)程可以支配它。為此,你只需在call( ) 定義前加上關(guān)鍵字synchronized,如下:
1
2
3
|
class Callme { synchronized void call(String msg) { ... |
這防止了在一個(gè)線(xiàn)程使用call( )時(shí)其他線(xiàn)程進(jìn)入call( )。在synchronized加到call( )前面以后,程序輸出如下:
1
2
3
|
[Hello] [Synchronized] [World] |
任何時(shí)候在多線(xiàn)程情況下,你有一個(gè)方法或多個(gè)方法操縱對(duì)象的內(nèi)部狀態(tài),都必須用synchronized 關(guān)鍵字來(lái)防止?fàn)顟B(tài)出現(xiàn)競(jìng)爭(zhēng)。記住,一旦線(xiàn)程進(jìn)入實(shí)例的同步方法,沒(méi)有其他線(xiàn)程可以進(jìn)入相同實(shí)例的同步方法。然而,該實(shí)例的其他不同步方法卻仍然可以被調(diào)用。
同步語(yǔ)句
盡管在創(chuàng)建的類(lèi)的內(nèi)部創(chuàng)建同步方法是獲得同步的簡(jiǎn)單和有效的方法,但它并非在任何時(shí)候都有效。這其中的原因,請(qǐng)跟著思考。假設(shè)你想獲得不為多線(xiàn)程訪(fǎng)問(wèn)設(shè)計(jì)的類(lèi)對(duì)象的同步訪(fǎng)問(wèn),也就是,該類(lèi)沒(méi)有用到synchronized方法。而且,該類(lèi)不是你自己,而是第三方創(chuàng)建的,你不能獲得它的源代碼。這樣,你不能在相關(guān)方法前加synchronized修飾符。怎樣才能使該類(lèi)的一個(gè)對(duì)象同步化呢?很幸運(yùn),解決方法很簡(jiǎn)單:你只需將對(duì)這個(gè)類(lèi)定義的方法的調(diào)用放入一個(gè)synchronized塊內(nèi)就可以了。
下面是synchronized語(yǔ)句的普通形式:
1
2
3
|
synchronized (object) { // statements to be synchronized } |
其中,object是被同步對(duì)象的引用。如果你想要同步的只是一個(gè)語(yǔ)句,那么不需要花括號(hào)。一個(gè)同步塊確保對(duì)object成員方法的調(diào)用僅在當(dāng)前線(xiàn)程成功進(jìn)入object管程后發(fā)生。
下面是前面程序的修改版本,在run( )方法內(nèi)用了同步塊:
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( )方法沒(méi)有被synchronized修飾。而synchronized是在Caller類(lèi)的run( )方法中聲明的。這可以得到上例中同樣正確的結(jié)果,因?yàn)槊總€(gè)線(xiàn)程運(yùn)行前都等待先前的一個(gè)線(xiàn)程結(jié)束。
Java線(xiàn)程間通信
多線(xiàn)程通過(guò)把任務(wù)分成離散的和合乎邏輯的單元代替了事件循環(huán)程序。線(xiàn)程還有第二優(yōu)點(diǎn):它遠(yuǎn)離了輪詢(xún)。輪詢(xún)通常由重復(fù)監(jiān)測(cè)條件的循環(huán)實(shí)現(xiàn)。一旦條件成立,就要采取適當(dāng)?shù)男袆?dòng)。這浪費(fèi)了CPU時(shí)間。舉例來(lái)說(shuō),考慮經(jīng)典的序列問(wèn)題,當(dāng)一個(gè)線(xiàn)程正在產(chǎn)生數(shù)據(jù)而另一個(gè)程序正在消費(fèi)它。為使問(wèn)題變得更有趣,假設(shè)數(shù)據(jù)產(chǎn)生器必須等待消費(fèi)者完成工作才能產(chǎn)生新的數(shù)據(jù)。在輪詢(xún)系統(tǒng),消費(fèi)者在等待生產(chǎn)者產(chǎn)生數(shù)據(jù)時(shí)會(huì)浪費(fèi)很多CPU周期。一旦生產(chǎn)者完成工作,它將啟動(dòng)輪詢(xún),浪費(fèi)更多的CPU時(shí)間等待消費(fèi)者的工作結(jié)束,如此下去。很明顯,這種情形不受歡迎。
為避免輪詢(xún),Java包含了通過(guò)wait( ),notify( )和notifyAll( )方法實(shí)現(xiàn)的一個(gè)進(jìn)程間通信機(jī)制。這些方法在對(duì)象中是用final方法實(shí)現(xiàn)的,所以所有的類(lèi)都含有它們。這三個(gè)方法僅在synchronized方法中才能被調(diào)用。盡管這些方法從計(jì)算機(jī)科學(xué)遠(yuǎn)景方向上來(lái)說(shuō)具有概念的高度先進(jìn)性,實(shí)際中用起來(lái)是很簡(jiǎn)單的:
wait( ) 告知被調(diào)用的線(xiàn)程放棄管程進(jìn)入睡眠直到其他線(xiàn)程進(jìn)入相同管程并且調(diào)用notify( )。
notify( ) 恢復(fù)相同對(duì)象中第一個(gè)調(diào)用 wait( ) 的線(xiàn)程。
notifyAll( ) 恢復(fù)相同對(duì)象中所有調(diào)用 wait( ) 的線(xiàn)程。具有最高優(yōu)先級(jí)的線(xiàn)程最先運(yùn)行。
這些方法在Object中被聲明,如下所示:
1
2
3
|
final void wait( ) throws InterruptedException final void notify( ) final void notifyAll( ) |
wait( )存在的另外的形式允許你定義等待時(shí)間。
下面的例子程序錯(cuò)誤的實(shí)行了一個(gè)簡(jiǎn)單生產(chǎn)者/消費(fèi)者的問(wèn)題。它由四個(gè)類(lèi)組成:Q,設(shè)法獲得同步的序列;Producer,產(chǎn)生排隊(duì)的線(xiàn)程對(duì)象;Consumer,消費(fèi)序列的線(xiàn)程對(duì)象;以及PC,創(chuàng)建單個(gè)Q,Producer,和Consumer的小類(lèi)。
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類(lèi)中的put( )和get( )方法是同步的,沒(méi)有東西阻止生產(chǎn)者超越消費(fèi)者,也沒(méi)有東西阻止消費(fèi)者消費(fèi)同樣的序列兩次。這樣,你就得到下面的錯(cuò)誤輸出(輸出將隨處理器速度和裝載的任務(wù)而改變):
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 |
生產(chǎn)者生成1后,消費(fèi)者依次獲得同樣的1五次。生產(chǎn)者在繼續(xù)生成2到7,消費(fèi)者沒(méi)有機(jī)會(huì)獲得它們。
用Java正確的編寫(xiě)該程序是用wait( )和notify( )來(lái)對(duì)兩個(gè)方向進(jìn)行標(biāo)志,如下所示:
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." ); } } |
內(nèi)部get( ), wait( )被調(diào)用。這使執(zhí)行掛起直到Producer 告知數(shù)據(jù)已經(jīng)預(yù)備好。這時(shí),內(nèi)部get( ) 被恢復(fù)執(zhí)行。獲取數(shù)據(jù)后,get( )調(diào)用notify( )。這告訴Producer可以向序列中輸入更多數(shù)據(jù)。在put( )內(nèi),wait( )掛起執(zhí)行直到Consumer取走了序列中的項(xiàng)目。當(dāng)執(zhí)行再繼續(xù),下一個(gè)數(shù)據(jù)項(xiàng)目被放入序列,notify( )被調(diào)用,這通知Consumer它應(yīng)該移走該數(shù)據(jù)。
下面是該程序的輸出,它清楚的顯示了同步行為:
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 |