java多線程設計模式

java語言已經内置了多線程支援,所有實作runnable接口的類都可被啟動一個新線程,新線程會執行該執行個體的run()方法,當run()方法執行完畢後,線程就結束了。一旦一個線程執行完畢,這個執行個體就不能再重新啟動,隻能重新生成一個新執行個體,再啟動一個新線程。
thread類是實作了runnable接口的一個執行個體,它代表一個線程的執行個體,并且,啟動線程的唯一方法就是通過thread類的start()執行個體方法:
thread t = new thread();
t.start();
start()方法是一個native方法,它将啟動一個新線程,并執行run()方法。thread類預設的run()方法什麼也不做就退出了。注意:直接調用run()方法并不會啟動一個新線程,它和調用一個普通的java方法沒有什麼差別。
是以,有兩個方法可以實作自己的線程:
方法1:自己的類extend thread,并複寫run()方法,就可以啟動新線程并執行自己定義的run()方法。例如:

public class mythread extends thread {

public run() {

system.out.println("mythread.run()");

}

}
在合适的地方啟動線程:new mythread().start();
方法2:如果自己的類已經extends另一個類,就無法直接extends thread,此時,必須實作一個runnable接口:
public class mythread extends otherclass implements runnable {
為了啟動mythread,需要首先執行個體化一個thread,并傳入自己的mythread執行個體:
mythread myt = new mythread();
thread t = new thread(myt);
事實上,當傳入一個runnable target參數給thread後,thread的run()方法就會調用target.run(),參考jdk源代碼:
public void run() {
if (target != null) {
target.run();
線程還有一些name, threadgroup, isdaemon等設定,由于和線程設計模式關聯很少,這裡就不多說了。
由于同一程序内的多個線程共享記憶體空間,在java中,就是共享執行個體,當多個線程試圖同時修改某個執行個體的内容時,就會造成沖突,是以,線程必須實作共享互斥,使多線程同步。
最簡單的同步是将一個方法标記為synchronized,對同一個執行個體來說,任一時刻隻能有一個synchronized方法在執行。當一個方法正在執行某個synchronized方法時,其他線程如果想要執行這個執行個體的任意一個synchronized方法,都必須等待目前執行 synchronized方法的線程退出此方法後,才能依次執行。
但是,非synchronized方法不受影響,不管目前有沒有執行synchronized方法,非synchronized方法都可以被多個線程同時執行。
此外,必須注意,隻有同一執行個體的synchronized方法同一時間隻能被一個線程執行,不同執行個體的synchronized方法是可以并發的。例如,class a定義了synchronized方法sync(),則不同執行個體a1.sync()和a2.sync()可以同時由兩個線程來執行。
多線程同步的實作最終依賴鎖機制。我們可以想象某一共享資源是一間屋子,每個人都是一個線程。當a希望進入房間時,他必須獲得門鎖,一旦a獲得門鎖,他進去後就立刻将門鎖上,于是b,c,d

就不得不在門外等待,直到a釋放鎖出來後,b,c,d中的某一人搶到了該鎖(具體搶法依賴于 jvm的實作,可以先到先得,也可以随機挑選),然後進屋又将門鎖上。這樣,任一時刻最多有一人在屋内(使用共享資源)。
java語言規範内置了對多線程的支援。對于java程式來說,每一個對象執行個體都有一把“鎖”,一旦某個線程獲得了該鎖,别的線程如果希望獲得該鎖,隻能等待這個線程釋放鎖之後。獲得鎖的方法隻有一個,就是synchronized關鍵字。例如:
public class sharedresource {
private int count = 0;
public int getcount() { return count; }
public synchronized void setcount(int count) { this.count = count; }
同步方法public synchronized void setcount(int count) { this.count = count; } 事實上相當于:
public void setcount(int count) {
synchronized(this) { // 在此獲得this鎖
this.count = count;
} // 在此釋放this鎖
紅色部分表示需要同步的代碼段,該區域為“危險區域”,如果兩個以上的線程同時執行,會引發沖突,是以,要更改sharedresource的内部狀态,必須先獲得sharedresource執行個體的鎖。
退出synchronized塊時,線程擁有的鎖自動釋放,于是,别的線程又可以擷取該鎖了。
為了提高性能,不一定要鎖定this,例如,sharedresource有兩個獨立變化的變量:
public class sharedresouce {
private int a = 0;
private int b = 0;
public synchronized void seta(int a) { this.a = a; }
public synchronized void setb(int b) { this.b = b; }
若同步整個方法,則seta()的時候無法setb(),setb()時無法seta()。為了提高性能,可以使用不同對象的鎖:
private object sync_a = new object();
private object sync_b = new object();
public void seta(int a) {
synchronized(sync_a) {
this.a = a;
}
public synchronized void setb(int b) {
synchronized(sync_b) {
this.b = b;
通常,多線程之間需要協調工作。例如,浏覽器的一個顯示圖檔的線程displaythread想要執行顯示圖檔的任務,必須等待下載下傳線程 downloadthread将該圖檔下載下傳完畢。如果圖檔還沒有下載下傳完,displaythread可以暫停,當downloadthread完成了任務後,再通知displaythread“圖檔準備完畢,可以顯示了”,這時,displaythread繼續執行。
以上邏輯簡單的說就是:如果條件不滿足,則等待。當條件滿足時,等待該條件的線程将被喚醒。在java中,這個機制的實作依賴于wait/notify。等待機制與鎖機制是密切關聯的。例如:
synchronized(obj) {
while(!condition) {
obj.wait();
obj.dosomething();
當線程a獲得了obj鎖後,發現條件condition不滿足,無法繼續下一處理,于是線程a就wait()。
在另一線程b中,如果b更改了某些條件,使得線程a的condition條件滿足了,就可以喚醒線程a:
condition = true;
obj.notify();
需要注意的概念是:
# 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) {} 代碼段内。
# 調用obj.wait()後,線程a就釋放了obj的鎖,否則線程b無法獲得obj鎖,也就無法在synchronized(obj) {} 代碼段内喚醒a。
# 當obj.wait()方法傳回後,線程a需要再次獲得obj鎖,才能繼續執行。
# 如果a1,a2,a3都在obj.wait(),則b調用obj.notify()隻能喚醒a1,a2,a3中的一個(具體哪一個由jvm決定)。
# obj.notifyall()則能全部喚醒a1,a2,a3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,是以,a1,a2,a3隻有一個有機會獲得鎖繼續執行,例如a1,其餘的需要等待a1釋放obj鎖之後才能繼續執行。
# 當b調用obj.notify/notifyall的時候,b正持有obj鎖,是以,a1,a2,a3雖被喚醒,但是仍無法獲得obj鎖。直到b退出synchronized塊,釋放obj鎖後,a1,a2,a3中的一個才有機會獲得鎖繼續執行。
前面講了wait/notify機制,thread還有一個sleep()靜态方法,它也能使線程暫停一段時間。sleep與wait的不同點是: sleep并不釋放鎖,并且sleep的暫停和wait暫停是不一樣的。obj.wait會使線程進入obj對象的等待集合中并等待喚醒。
但是wait()和sleep()都可以通過interrupt()方法打斷線程的暫停狀态,進而使線程立刻抛出interruptedexception。
如果線程a希望立即結束線程b,則可以對線程b對應的thread執行個體調用interrupt方法。如果此刻線程b正在 wait/sleep/join,則線程b會立刻抛出interruptedexception,在catch() {} 中直接return即可安全地結束線程。
需要注意的是,interruptedexception是線程自己從内部抛出的,并不是interrupt()方法抛出的。對某一線程調用 interrupt()時,如果該線程正在執行普通的代碼,那麼該線程根本就不會抛出interruptedexception。但是,一旦該線程進入到 wait()/sleep()/join()後,就會立刻抛出interruptedexception。
guardedsuspention模式主要思想是:
當條件不滿足時,線程等待,直到條件滿足時,等待該條件的線程被喚醒。
我們設計一個用戶端線程和一個伺服器線程,用戶端線程不斷發送請求給伺服器線程,伺服器線程不斷處理請求。當請求隊列為空時,伺服器線程就必須等待,直到用戶端發送了請求。
先定義一個請求隊列:queue
package com.crackj2ee.thread;
import java.util.*;
public class queue {
private list queue = new linkedlist();
public synchronized request getrequest() {
while(queue.size()==0) {
try {
this.wait();
}
catch(interruptedexception ie) {
return null;
return (request)queue.remove(0);
public synchronized void putrequest(request request) {
queue.add(request);
this.notifyall();
藍色部分就是伺服器線程的等待條件,而用戶端線程在放入了一個request後,就使伺服器線程等待條件滿足,于是喚醒伺服器線程。
用戶端線程:clientthread
public class clientthread extends thread {
private queue queue;
private string clientname;
public clientthread(queue queue, string clientname) {
this.queue = queue;
this.clientname = clientname;
public string tostring() {
return "[clientthread-" + clientname + "]";
public void run() {
for(int i=0; i<100; i++) {
request request = new request("" + (long)(math.random()*10000));
system.out.println(this + " send request: " + request);
queue.putrequest(request);
thread.sleep((long)(math.random() * 10000 + 1000));
system.out.println(this + " shutdown.");
伺服器線程:serverthread
public class serverthread extends thread {
private boolean stop = false;
public serverthread(queue queue) {
public void shutdown() {
stop = true;
this.interrupt();
try {
this.join();
catch(interruptedexception ie) {}
while(!stop) {
request request = queue.getrequest();
system.out.println("[serverthread] handle request: " + request);
thread.sleep(2000);
catch(interruptedexception ie) {}
system.out.println("[serverthread] shutdown.");
伺服器線程在紅色部分可能會阻塞,也就是說,queue.getrequest是一個阻塞方法。這和java标準庫的許多io方法類似。
最後,寫一個main來啟動他們:
public class main {
public static void main(string[] args) {
queue queue = new queue();
serverthread server = new serverthread(queue);
server.start();
clientthread[] clients = new clientthread[5];
for(int i=0; i<clients.length; i++) {
clients[i] = new clientthread(queue, ""+i);
clients[i].start();
thread.sleep(100000);
server.shutdown();
我們啟動了5個用戶端線程和一個伺服器線程,運作結果如下:
[clientthread-0] send request: request-4984
[serverthread] handle request: request-4984
[clientthread-1] send request: request-2020
[clientthread-2] send request: request-8980
[clientthread-3] send request: request-5044
[clientthread-4] send request: request-548
[clientthread-4] send request: request-6832
[serverthread] handle request: request-2020
[serverthread] handle request: request-8980
[serverthread] handle request: request-5044
[serverthread] handle request: request-548
[clientthread-4] send request: request-1681
[clientthread-0] send request: request-7859
[clientthread-3] send request: request-3926
[serverthread] handle request: request-6832
[clientthread-2] send request: request-9906
可以觀察到serverthread處理來自不同用戶端的請求。
思考
q: 伺服器線程的wait條件while(queue.size()==0)能否換成if(queue.size()==0)?
a: 在這個例子中可以,因為伺服器線程隻有一個。但是,如果伺服器線程有多個(例如web應用程式有多個線程處理并發請求,這非常普遍),就會造成嚴重問題。
q: 能否用sleep(1000)代替wait()?
a: 絕對不可以。sleep()不會釋放鎖,是以sleep期間别的線程根本沒有辦法調用getrequest()和putrequest(),導緻所有相關線程都被阻塞。
q: (request)queue.remove(0)可以放到synchronized() {}塊外面嗎?
a: 不可以。因為while()是測試queue,remove()是使用queue,兩者是一個原子操作,不能放在synchronized外面。
總結
多線程設計看似簡單,實際上必須非常仔細地考慮各種鎖定/同步的條件,稍不小心,就可能出錯。并且,當線程較少時,很可能發現不了問題,一旦問題出現又難以調試。
所幸的是,已有一些被驗證過的模式可以供我們使用,我們會繼續介紹一些常用的多線程設計模式。
前面談了多線程應用程式能極大地改善使用者相應。例如對于一個web應用程式,每當一個使用者請求伺服器連接配接時,伺服器就可以啟動一個新線程為使用者服務。
然而,建立和銷毀線程本身就有一定的開銷,如果頻繁建立和銷毀線程,cpu和記憶體開銷就不可忽略,垃圾收集器還必須負擔更多的工作。是以,線程池就是為了避免頻繁建立和銷毀線程。
每當伺服器接受了一個新的請求後,伺服器就從線程池中挑選一個等待的線程并執行請求處理。處理完畢後,線程并不結束,而是轉為阻塞狀态再次被放入線程池中。這樣就避免了頻繁建立和銷毀線程。
worker pattern實作了類似線程池的功能。首先定義task接口:
public interface task {
void execute();
線程将負責執行execute()方法。注意到任務是由子類通過實作execute()方法實作的,線程本身并不知道自己執行的任務。它隻負責運作一個耗時的execute()方法。
具體任務由子類實作,我們定義了一個calculatetask和一個timertask:
// calculatetask.java
public class calculatetask implements task {
private static int count = 0;
private int num = count;
public calculatetask() {
count++;
public void execute() {
system.out.println("[calculatetask " + num + "] start");
thread.sleep(3000);
system.out.println("[calculatetask " + num + "] done.");
// timertask.java
public class timertask implements task {
public timertask() {
system.out.println("[timertask " + num + "] start");
thread.sleep(2000);
system.out.println("[timertask " + num + "] done.");
以上任務均簡單的sleep若幹秒。
taskqueue實作了一個隊列,用戶端可以将請求放入隊列,伺服器線程可以從隊列中取出任務:
public class taskqueue {
public synchronized task gettask() {
return (task)queue.remove(0);
public synchronized void puttask(task task) {
queue.add(task);
終于到了真正的workerthread,這是真正執行任務的伺服器線程:
public class workerthread extends thread {
private boolean busy = false;
private taskqueue queue;
public workerthread(threadgroup group, taskqueue queue) {
super(group, "worker-" + count);
public boolean isidle() {
return !busy;
system.out.println(getname() + " start.");
task task = queue.gettask();
if(task!=null) {
busy = true;
task.execute();
busy = false;
system.out.println(getname() + " end.");
前面已經講過,queue.gettask()是一個阻塞方法,伺服器線程可能在此wait()一段時間。此外,workerthread還有一個shutdown方法,用于安全結束線程。
最後是threadpool,負責管理所有的伺服器線程,還可以動态增加和減少線程數:
public class threadpool extends threadgroup {
private list threads = new linkedlist();
public threadpool(taskqueue queue) {
super("thread-pool");
public synchronized void addworkerthread() {
thread t = new workerthread(this, queue);
threads.add(t);
t.start();
public synchronized void removeworkerthread() {
if(threads.size()>0) {
workerthread t = (workerthread)threads.remove(0);
t.shutdown();
public synchronized void currentstatus() {
system.out.println("-----------------------------------------------");
system.out.println("thread count = " + threads.size());
iterator it = threads.iterator();
while(it.hasnext()) {
workerthread t = (workerthread)it.next();
system.out.println(t.getname() + ": " + (t.isidle() ? "idle" : "busy"));
currentstatus()方法是為了友善調試,列印出所有線程的目前狀态。
最後,main負責完成main()方法:
taskqueue queue = new taskqueue();
threadpool pool = new threadpool(queue);
for(int i=0; i<10; i++) {
queue.puttask(new calculatetask());
queue.puttask(new timertask());
pool.addworkerthread();
dosleep(8000);
pool.currentstatus();
dosleep(5000);
private static void dosleep(long ms) {
thread.sleep(ms);
main()一開始放入了20個task,然後動态添加了一些服務線程,并定期列印線程狀态,運作結果如下:
worker-0 start.
[calculatetask 0] start
worker-1 start.
[timertask 0] start
[timertask 0] done.
[calculatetask 1] start
[calculatetask 0] done.
[timertask 1] start
[calculatetask 1] done.
[calculatetask 2] start
[timertask 1] done.
[timertask 2] start
[timertask 2] done.
[calculatetask 3] start
-----------------------------------------------
thread count = 2
worker-0: busy
worker-1: busy
[calculatetask 2] done.
[timertask 3] start
worker-2 start.
[calculatetask 4] start
worker-3 start.
[timertask 4] start
worker-4 start.
[calculatetask 5] start
worker-5 start.
[timertask 5] start
worker-6 start.
[calculatetask 6] start
[calculatetask 3] done.
[timertask 6] start
[timertask 3] done.
[calculatetask 7] start
[timertask 4] done.
[timertask 7] start
[timertask 5] done.
[calculatetask 8] start
[calculatetask 4] done.
[timertask 8] start
[calculatetask 5] done.
[calculatetask 9] start
[calculatetask 6] done.
[timertask 9] start
[timertask 6] done.
[timertask 7] done.
thread count = 7
worker-0: idle
worker-2: busy
worker-3: idle
worker-4: busy
worker-5: busy
worker-6: busy
[calculatetask 7] done.
[calculatetask 8] done.
[timertask 8] done.
[timertask 9] done.
[calculatetask 9] done.
仔細觀察:一開始隻有兩個伺服器線程,是以線程狀态都是忙,後來線程數增多,6個線程中的兩個狀态變成idle,說明處于wait()狀态。
思考:本例的線程排程算法其實根本沒有,因為這個應用是圍繞taskqueue設計的,不是以thread pool為中心設計的。是以,task排程取決于taskqueue的gettask()方法,你可以改進這個方法,例如使用優先隊列,使優先級高的任務先被執行。
如果所有的伺服器線程都處于busy狀态,則說明任務繁忙,taskqueue的隊列越來越長,最終會導緻伺服器記憶體耗盡。是以,可以限制 taskqueue的等待任務數,超過最大長度就拒絕處理。許多web伺服器在使用者請求繁忙時就會拒絕使用者:http 503 service unavailable
多線程讀寫同一個對象的資料是很普遍的,通常,要避免讀寫沖突,必須保證任何時候僅有一個線程在寫入,有線程正在讀取的時候,寫入操作就必須等待。簡單說,就是要避免“寫-寫”沖突和“讀-寫”沖突。但是同時讀是允許的,因為“讀-讀”不沖突,而且很安全。
要實作以上的readwritelock,簡單的使用synchronized就不行,我們必須自己設計一個readwritelock類,在讀之前,必須先獲得“讀鎖”,寫之前,必須先獲得“寫鎖”。舉例說明:
datahandler對象儲存了一個可讀寫的char[]數組:
public class datahandler {
// store data:
private char[] buffer = "aaaaaaaaaa".tochararray();
private char[] doread() {
char[] ret = new char[buffer.length];
for(int i=0; i<buffer.length; i++) {
ret[i] = buffer[i];
sleep(3);
return ret;
private void dowrite(char[] data) {
if(data!=null) {
buffer = new char[data.length];
for(int i=0; i<buffer.length; i++) {
buffer[i] = data[i];
sleep(10);
private void sleep(int ms) {
doread()和dowrite()方法是非線程安全的讀寫方法。為了示範,加入了sleep(),并設定讀的速度大約是寫的3倍,這符合通常的情況。
為了讓多線程能安全讀寫,我們設計了一個readwritelock:
public class readwritelock {
private int readingthreads = 0;
private int writingthreads = 0;
private int waitingthreads = 0; // waiting for write
private boolean preferwrite = true;
public synchronized void readlock() throws interruptedexception {
while(writingthreads>0 || (preferwrite && waitingthreads>0))
this.wait();
readingthreads++;
public synchronized void readunlock() {
readingthreads--;
preferwrite = true;
notifyall();
public synchronized void writelock() throws interruptedexception {
waitingthreads++;
while(readingthreads>0 || writingthreads>0)
finally {
waitingthreads--;
writingthreads++;
public synchronized void writeunlock() {
writingthreads--;
preferwrite = false;
readlock()用于獲得讀鎖,readunlock()釋放讀鎖,writelock()和writeunlock()一樣。由于鎖用完必須釋放,是以,必須保證lock和unlock比對。我們修改datahandler,加入readwritelock:
// lock:
private readwritelock lock = new readwritelock();
public char[] read(string name) throws interruptedexception {
system.out.println(name + " waiting for read");
lock.readlock();
char[] data = doread();
system.out.println(name + " reads data: " + new string(data));
return data;
lock.readunlock();
public void write(string name, char[] data) throws interruptedexception {
system.out.println(name + " waiting for write");
lock.writelock();
system.out.println(name + " wrote data: " + new string(data));
dowrite(data);
lock.writeunlock();
public方法read()和write()完全封裝了底層的readwritelock,是以,多線程可以安全地調用這兩個方法:
// readingthread不斷讀取資料:
public class readingthread extends thread {
private datahandler handler;
public readingthread(datahandler handler) {
this.handler = handler;
for(;;) {
char[] data = handler.read(getname());
thread.sleep((long)(math.random()*1000+100));
break;
// writingthread不斷寫入資料,每次寫入的都是10個相同的字元:
public class writingthread extends thread {
public writingthread(datahandler handler) {
char[] data = new char[10];
fill(data);
handler.write(getname(), data);
// 産生一個a-z随機字元,填入char[10]:
private void fill(char[] data) {
char c = (char)(math.random()*26+'a');
for(int i=0; i<data.length; i++)
data[i] = c;
最後main負責啟動這些線程:
datahandler handler = new datahandler();
thread[] ts = new thread[] {
new readingthread(handler),
new writingthread(handler),
new writingthread(handler)
};
for(int i=0; i<ts.length; i++) {
ts[i].start();
我們啟動了5個讀線程和2個寫線程,運作結果如下:
thread-0 waiting for read
thread-1 waiting for read
thread-2 waiting for read
thread-3 waiting for read
thread-4 waiting for read
thread-5 waiting for write
thread-6 waiting for write
thread-4 reads data: aaaaaaaaaa
thread-3 reads data: aaaaaaaaaa
thread-2 reads data: aaaaaaaaaa
thread-1 reads data: aaaaaaaaaa
thread-0 reads data: aaaaaaaaaa
thread-5 wrote data: eeeeeeeeee
thread-6 wrote data: mmmmmmmmmm
thread-1 reads data: mmmmmmmmmm
thread-4 reads data: mmmmmmmmmm
thread-2 reads data: mmmmmmmmmm
thread-0 reads data: mmmmmmmmmm
thread-5 wrote data: gggggggggg
thread-6 wrote data: aaaaaaaaaa
可以看到,每次讀/寫都是完整的原子操作,因為我們每次寫入的都是10個相同字元。并且,每次讀出的都是最近一次寫入的内容。
如果去掉readwritelock:
char[] data = doread();
system.out.println(name + " reads data: " + new string(data));
return data;
system.out.println(name + " wrote data: " + new string(data));
dowrite(data);
char[] ret = new char[10];
buffer[i] = data[i];
sleep(10);
運作結果如下:
thread-5 wrote data: aaaaaaaaaa
thread-2 reads data: maaaaaaaaa
thread-3 reads data: maaaaaaaaa
thread-5 wrote data: cccccccccc
thread-1 reads data: maaaaaaaaa
thread-0 reads data: maaaaaaaaa
thread-4 reads data: maaaaaaaaa
thread-6 wrote data: eeeeeeeeee
thread-3 reads data: eeeeeccccc
thread-4 reads data: eeeeeeeeec
thread-1 reads data: eeeeeeeeee
可以看到在thread-6寫入eeeeeeeeee的過程中,3個線程讀取的内容是不同的。
java的synchronized提供了最底層的實體鎖,要在synchronized的基礎上,實作自己的邏輯鎖,就必須仔細設計readwritelock。
q: lock.readlock()為什麼不放入try{ } 内?
a: 因為readlock()會抛出interruptedexception,導緻readingthreads++不執行,而readunlock()在 finally{ } 中,導緻readingthreads--執行,進而使readingthread狀态出錯。writelock()也是類似的。
q: preferwrite有用嗎?
a: 如果去掉preferwrite,線程安全不受影響。但是,如果讀取線程很多,上一個線程還沒有讀取完,下一個線程又開始讀了,就導緻寫入線程長時間無法獲得writelock;如果寫入線程等待的很多,一個接一個寫,也會導緻讀取線程長時間無法獲得readlock。preferwrite的作用是讓讀 /寫交替執行,避免由于讀線程繁忙導緻寫無法進行和由于寫線程繁忙導緻讀無法進行。
q: notifyall()換成notify()行不行?
a: 不可以。由于preferwrite的存在,如果一個線程剛讀取完畢,此時preferwrite=true,再notify(),若恰好喚醒的是一個讀線程,則while(writingthreads>0 || (preferwrite && waitingthreads>0))可能為true導緻該讀線程繼續等待,而等待寫入的線程也處于wait()中,結果所有線程都處于wait ()狀态,誰也無法喚醒誰。是以,notifyall()比notify()要來得安全。程式驗證notify()帶來的死鎖:
thread-6 wrote data: llllllllll
thread-2 reads data: llllllllll
(運作到此不動了)
注意到這種死鎖是由于所有線程都在等待别的線程喚醒自己,結果都無法醒過來。這和兩個線程希望獲得對方已有的鎖造成死鎖不同。是以多線程設計的難度遠遠高于單線程應用。
特别說明:尊重作者的勞動成果,轉載請注明出處哦~~~http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt220