四種方式 sychronized關(guān)鍵字
- sychronized method(){}
- sychronized (objectReference) {/*block*/}
- static synchronized method(){}
- sychronized(classname.class)
其中1和2是代表鎖當(dāng)前對(duì)象,即一個(gè)對(duì)象就一個(gè)鎖,3和4代表鎖這個(gè)類,即這個(gè)類的鎖
要注意的是sychronized method()不是鎖這個(gè)函數(shù),而是鎖對(duì)象,即:如果這個(gè)類中有兩個(gè)方法都是sychronized,那么只要有兩個(gè)線程共享一個(gè)該類的reference,每個(gè)調(diào)用這兩個(gè)方法之一,不管是否同一個(gè)方法,都會(huì)用這個(gè)對(duì)象鎖進(jìn)行同步。鎖類的3和4類推,即該類的不同reference調(diào)用了sychronized區(qū)段的咚咚就會(huì)受類鎖的控制
還有,如果兩個(gè)函數(shù)調(diào)用的先后順序不能被打斷,那么可以有個(gè)專門的鎖對(duì)象來完成這個(gè)任務(wù):
class MyLock
{
synchronized getLock()
{
//####還沒寫完
}
}
五個(gè)等級(jí) 參見effective java Item 52 : Document thread safety
- immutable 不可變對(duì)象
- thread-safe 線程安全的,可以放心使用,如java.util.Timer
- conditionally thread-safe 條件線程安全的,如Vector和Hashtable,一般是安全的,除非存在幾個(gè)方法調(diào)用之間的順序不能被打斷,這時(shí)可以用額外的鎖來完成
- thread-compatible 可以使用synchronized (objectReference)來協(xié)助完成對(duì)線程的調(diào)用
- thread-hostile 不安全的
wait & notifyAll
在循環(huán)中使用wait 使用notifyAll而不是notify
pipe
java中也有pipe的,四個(gè)類:PipedInputStream, PipedInputReader, PipedOutputStream, PipedOutputWriter 下面是一段生產(chǎn)者消費(fèi)者的代碼(摘自core javaII):
/* set up pipes */
PipedOutputStream pout1 = new PipedOutputStream();
PipedInputStream pin1 = new PipedInputStream(pout1);
PipedOutputStream pout2 = new PipedOutputStream();
PipedInputStream pin2 = new PipedInputStream(pout2);
/* construct threads */
Producer prod = new Producer(pout1);
Filter filt = new Filter(pin1, pout2);
Consumer cons = new Consumer(pin2);
/* start threads */
prod.start(); filt.start(); cons.start();
注意
long 和double是簡單類型中兩個(gè)特殊的咚咚:java讀他們要讀兩次,所以需要同步
死鎖是一個(gè)經(jīng)典的多線程問題,因?yàn)椴煌木€程都在等待那些根本不可能被釋放的鎖,從而導(dǎo)致所有的工作都無法完成。假設(shè)有兩個(gè)線程,分別代表兩個(gè)饑餓的人,他們必須共享刀叉并輪流吃飯。他們都需要獲得兩個(gè)鎖:共享刀和共享叉的鎖。假如線程 "A" 獲得了刀,而線程 "B" 獲得了叉。線程 A 就會(huì)進(jìn)入阻塞狀態(tài)來等待獲得叉,而線程 B 則阻塞來等待 A 所擁有的刀。這只是人為設(shè)計(jì)的例子,但盡管在運(yùn)行時(shí)很難探測(cè)到,這類情況卻時(shí)常發(fā)生。雖然要探測(cè)或推敲各種情況是非常困難的,但只要按照下面幾條規(guī)則去設(shè)計(jì)系統(tǒng),就能夠避免死鎖問題:
- 讓所有的線程按照同樣的順序獲得一組鎖。這種方法消除了 X 和 Y 的擁有者分別等待對(duì)方的資源的問題。
-
- 將多個(gè)鎖組成一組并放到同一個(gè)鎖下。前面死鎖的例子中,可以創(chuàng)建一個(gè)銀器對(duì)象的鎖。于是在獲得刀或叉之前都必須獲得這個(gè)銀器的鎖。
-
- 將那些不會(huì)阻塞的可獲得資源用變量標(biāo)志出來。當(dāng)某個(gè)線程獲得銀器對(duì)象的鎖時(shí),就可以通過檢查變量來判斷是否整個(gè)銀器集合中的對(duì)象鎖都可獲得。如果是,它就可以獲得相關(guān)的鎖,否則,就要釋放掉銀器這個(gè)鎖并稍后再嘗試。
-
- 最重要的是,在編寫代碼前認(rèn)真仔細(xì)地設(shè)計(jì)整個(gè)系統(tǒng)。多線程是困難的,在開始編程之前詳細(xì)設(shè)計(jì)系統(tǒng)能夠幫助你避免難以發(fā)現(xiàn)死鎖的問題。
Volatile 變量. volatile
關(guān)鍵字是 Java 語言為優(yōu)化編譯器設(shè)計(jì)的。以下面的代碼為例:
class VolatileTest { public void foo() { boolean flag = false; if(flag) { //this could happen } } } |
一個(gè)優(yōu)化的編譯器可能會(huì)判斷出 if
部分的語句永遠(yuǎn)不會(huì)被執(zhí)行,就根本不會(huì)編譯這部分的代碼。如果這個(gè)類被多線程訪問, flag
被前面某個(gè)線程設(shè)置之后,在它被 if
語句測(cè)試之前,可以被其他線程重新設(shè)置。用 volatile
關(guān)鍵字來聲明變量,就可以告訴編譯器在編譯的時(shí)候,不需要通過預(yù)測(cè)變量值來優(yōu)化這部分的代碼。
無法訪問的線程 有時(shí)候雖然獲取對(duì)象鎖沒有問題,線程依然有可能進(jìn)入阻塞狀態(tài)。在 Java 編程中 IO 就是這類問題最好的例子。當(dāng)線程因?yàn)閷?duì)象內(nèi)的 IO 調(diào)用而阻塞時(shí),此對(duì)象應(yīng)當(dāng)仍能被其他線程訪問。該對(duì)象通常有責(zé)任取消這個(gè)阻塞的 IO 操作。造成阻塞調(diào)用的線程常常會(huì)令同步任務(wù)失敗。如果該對(duì)象的其他方法也是同步的,當(dāng)線程被阻塞時(shí),此對(duì)象也就相當(dāng)于被冷凍住了。其他的線程由于不能獲得對(duì)象的鎖,就不能給此對(duì)象發(fā)消息(例如,取消 IO 操作)。必須確保不在同步代碼中包含那些阻塞調(diào)用,或確認(rèn)在一個(gè)用同步阻塞代碼的對(duì)象中存在非同步方法。盡管這種方法需要花費(fèi)一些注意力來保證結(jié)果代碼安全運(yùn)行,但它允許在擁有對(duì)象的線程發(fā)生阻塞后,該對(duì)象仍能夠響應(yīng)其他線程。
調(diào)用 yield()
方法能夠?qū)?dāng)前的線程從處理器中移出到準(zhǔn)備就緒隊(duì)列中。另一個(gè)方法則是調(diào)用 sleep()
方法,使線程放棄處理器,并且在 sleep 方法中指定的時(shí)間間隔內(nèi)睡眠。
正如你所想的那樣,將這些方法隨意放在代碼的某個(gè)地方,并不能夠保證正常工作。如果線程正擁有一個(gè)鎖(因?yàn)樗谝粋€(gè)同步方法或代碼塊中),則當(dāng)它調(diào)用 yield()
時(shí)不能夠釋放這個(gè)鎖。這就意味著即使這個(gè)線程已經(jīng)被掛起,等待這個(gè)鎖釋放的其他線程依然不能繼續(xù)運(yùn)行。為了緩解這個(gè)問題,最好不在同步方法中調(diào)用 yield
方法。將那些需要同步的代碼包在一個(gè)同步塊中,里面不含有非同步的方法,并且在這些同步代碼塊之外才調(diào)用 yield
。
另外一個(gè)解決方法則是調(diào)用 wait()
方法,使處理器放棄它當(dāng)前擁有的對(duì)象的鎖。如果對(duì)象在方法級(jí)別上使同步的,這種方法能夠很好的工作。因?yàn)樗鼉H僅使用了一個(gè)鎖。如果它使用 fine-grained 鎖,則 wait()
將無法放棄這些鎖。此外,一個(gè)因?yàn)檎{(diào)用 wait()
方法而阻塞的線程,只有當(dāng)其他線程調(diào)用 notifyAll()
時(shí)才會(huì)被喚醒。
在進(jìn)行多線程編程時(shí),經(jīng)常要使用同步互斥機(jī)構(gòu),但java本身沒有提供的同步互斥機(jī)構(gòu),僅提供了兩個(gè)與同步互斥有關(guān)的方法:wait()和notify(),可以用來設(shè)計(jì)信號(hào)量類:mySemaphore,它是按照Dijkstra提出的計(jì)數(shù)信號(hào)量的思想設(shè)計(jì)的。
mySemaphore有兩個(gè)最重要的成員方法:P()和V()。這兩個(gè)方法實(shí)際就實(shí)現(xiàn)了信號(hào)量的P操作和V操作。具體描述如下:
public synchronized void P(){
semaphore--;
if(semaphore<0){
try{
wait();
}catch(InterruptedException ie){}
}
}
public synchronized void V(){
semaphore++;
if(semaphore<=0)
notify();
}
其中,semaphore變量記錄了信號(hào)量的狀態(tài),wait()方法相當(dāng)于block原語,用于阻塞線程的執(zhí)行,notify()方法相當(dāng)于wakeup原語,用于喚醒線程恢復(fù)運(yùn)行。由于這兩個(gè)方法定義為synchronized,這樣java虛擬機(jī)可保證這兩個(gè)方法的原子執(zhí)行,從而實(shí)現(xiàn)了P、V操作。
二、管道
并發(fā)程序的多個(gè)線程之間的通訊通常是使用管道進(jìn)行,jdk提供了兩個(gè)管道類:PipedInpuStream和PipedOutputStream,前者用于輸入,后者用于輸出。這兩種管道應(yīng)該是能夠多次連接和關(guān)閉,在實(shí)現(xiàn)過程中,卻發(fā)現(xiàn)它們?cè)陉P(guān)閉后,不能重新建立連接。經(jīng)過仔細(xì)調(diào)試后,發(fā)現(xiàn)jdk的源代碼在處理關(guān)閉時(shí)釋放資源存在著缺陷,因此需要編寫自己的管道類:MyPipedInputStream和MyPipedOutputStream。這兩個(gè)類直接從InputStream和OutputStream繼承而來,其成員方法與實(shí)現(xiàn)基本與PipedInputStream和PipedOutputStream一致,只是在處理關(guān)閉時(shí),將類中的成員變量的值恢復(fù)成未連接時(shí)的初始值。另外,原有的管道了提供的管道容量只有1024個(gè)字節(jié),在傳輸數(shù)據(jù)量較大時(shí),可能會(huì)發(fā)生溢出,而在自己的管道類中可以任意設(shè)置管道容量,例如可以根據(jù)需要把管道容量設(shè)為64KB。以下僅給出了相應(yīng)的關(guān)閉例程:
1.MyPipedInputStream
public void close() throws IOException {
in = -1;
out = 0;
closedByReader = true;
connected = false;
closed = true;
buffer = new byte[PIPE_SIZE];
}
2.MyPipedOutputStream
public void close() throws IOException {
if (sink != null) {
sink.receivedLast();
sink.closed = true;
}
sink = null;
connected = false;
}