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