如果這個(gè)例子還不能幫助你理解如何解決多線程的問題,那么下面再來看一個(gè)更加實(shí)際的例子——衛(wèi)生間問題。
例
如火車上車廂的衛(wèi)生間,為了簡單,這里只模擬一個(gè)衛(wèi)生間,這個(gè)衛(wèi)生間會(huì)被多個(gè)人同時(shí)使用,在實(shí)際使用時(shí),當(dāng)一個(gè)人進(jìn)入衛(wèi)生間時(shí)則會(huì)把衛(wèi)生間鎖上,等出來時(shí)
打開門,下一個(gè)人進(jìn)去把門鎖上,如果有一個(gè)人在衛(wèi)生間內(nèi)部則別人的人發(fā)現(xiàn)門是鎖的則只能在外面等待。從編程的角度來看,這里的每個(gè)人都可以看作是一個(gè)線程
對(duì)象,而這個(gè)衛(wèi)生間對(duì)象由于被多個(gè)線程訪問,則就是臨界資源,在一個(gè)線程實(shí)際使用時(shí),使用synchronized關(guān)鍵將臨界資源鎖定,當(dāng)結(jié)束時(shí),釋放鎖定。實(shí)現(xiàn)的代碼如下:
package syn3;
/**
* 測試類
*/
public class TestHuman {
public static void main(String[] args) {
Toilet t = new Toilet(); //衛(wèi)生間對(duì)象
Human h1 = new Human("1",t);
Human h2 = new Human("2",t);
Human h3 = new Human("3",t);
}
}
package syn3;
/**
* 人線程類,演示互斥
*/
public class Human extends Thread {
Toilet t;
String name;
public Human(String name,Toilet t){
this.name = name;
this.t = t;
start(); //啟動(dòng)線程
}
public void run(){
//進(jìn)入衛(wèi)生間
t.enter(name);
}
}
package syn3;
/**
* 衛(wèi)生間,互斥的演示
*/
public class Toilet {
public synchronized void enter(String name){
System.out.println(name + "已進(jìn)入!");
try{
Thread.sleep(2000);
}catch(Exception e){}
System.out.println(name + "離開!");
}
}
該示例的執(zhí)行結(jié)果為,不同次數(shù)下執(zhí)行結(jié)果會(huì)有所不同:
1已進(jìn)入!
1離開!
3已進(jìn)入!
3離開!
2已進(jìn)入!
2離開!
在該示例代碼中,Toilet類表示衛(wèi)生間類,Human類模擬人,是該示例中的線程類,TestHuman類是測試類,用于啟動(dòng)線程。在TestHuman中,首先創(chuàng)建一個(gè)Toilet類型的對(duì)象t,并將該對(duì)象傳遞到后續(xù)創(chuàng)建的線程對(duì)象中,這樣后續(xù)的線程對(duì)象就使用同一個(gè)Toilet對(duì)象,該對(duì)象就成為了臨界資源。下面創(chuàng)建了三個(gè)Human類型的線程對(duì)象,每個(gè)線程具有自己的名稱name參數(shù),模擬3個(gè)線程,在每個(gè)線程對(duì)象中,只是調(diào)用對(duì)象t中的enter方法,模擬進(jìn)入衛(wèi)生間的動(dòng)作,在enter方法中,在進(jìn)入時(shí)輸出調(diào)用該方法的線程進(jìn)入,然后延遲2秒,輸出該線程離開,然后后續(xù)的一個(gè)線程進(jìn)入,直到三個(gè)線程都完成enter方法則程序結(jié)束。
在該示例中,同一個(gè)Toilet類的對(duì)象t的enter方法由于具有synchronized修飾符修飾,則在多個(gè)線程同時(shí)調(diào)用該方法時(shí),如果一個(gè)線程進(jìn)入到enter方法內(nèi)部,則為對(duì)象t上鎖,直到enter方法結(jié)束以后釋放對(duì)該對(duì)象的鎖定,通過這種方式實(shí)現(xiàn)無論多少個(gè)Human類型的線程,對(duì)于同一個(gè)對(duì)象t,任何時(shí)候只能有一個(gè)線程執(zhí)行enter方法,這就是解決多線程問題的第一種思路——互斥的解決原理。
12.4.2 同步
使用互斥解決多線程問題是一種簡單有效的解決辦法,但是由于該方法比較簡單,所以只能解決一些基本的問題,對(duì)于復(fù)雜的問題就無法解決了。
解
決多線程問題的另外一種思路是同步。同步是另外一種解決問題的思路,結(jié)合前面衛(wèi)生間的示例,互斥方式解決多線程的原理是,當(dāng)一個(gè)人進(jìn)入到衛(wèi)生間內(nèi)部時(shí),別
的人只能在外部時(shí)刻等待,這樣就相當(dāng)于別的人雖然沒有事情做,但是還是要占用別的人的時(shí)間,浪費(fèi)系統(tǒng)的執(zhí)行資源。而同步解決問題的原理是,如果一個(gè)人進(jìn)入
到衛(wèi)生間內(nèi)部時(shí),則別的人可以去睡覺,不占用系統(tǒng)資源,而當(dāng)這個(gè)人從衛(wèi)生間出來以后,把這個(gè)睡覺的人叫醒,則它就可以使用臨界資源了。所以使用同步的思路
解決多線程問題更加有效,更加節(jié)約系統(tǒng)的資源。
在常見的多線程問題解決中,同步問題的典型示例是“生產(chǎn)者-消費(fèi)者”模型,也就是生產(chǎn)者線程只負(fù)責(zé)生產(chǎn),消費(fèi)者線程只負(fù)責(zé)消費(fèi),在消費(fèi)者發(fā)現(xiàn)無內(nèi)容可消費(fèi)時(shí)則睡覺。下面舉一個(gè)比較實(shí)際的例子——生活費(fèi)問題。
生
活費(fèi)問題是這樣的:學(xué)生每月都需要生活費(fèi),家長一次預(yù)存一段時(shí)間的生活費(fèi),家長和學(xué)生使用統(tǒng)一的一個(gè)帳號(hào),在學(xué)生每次取帳號(hào)中一部分錢,直到帳號(hào)中沒錢時(shí)
通知家長存錢,而家長看到帳戶還有錢則不存錢,直到帳戶沒錢時(shí)才存錢。在這個(gè)例子中,這個(gè)帳號(hào)被學(xué)生和家長兩個(gè)線程同時(shí)訪問,則帳號(hào)就是臨界資源,兩個(gè)線
程是同時(shí)執(zhí)行的,當(dāng)每個(gè)線程發(fā)現(xiàn)不符合要求時(shí)則等待,并釋放分配給自己的CPU執(zhí)行時(shí)間,也就是不占用系統(tǒng)資源。實(shí)現(xiàn)該示例的代碼為:
package syn4;
/**
* 測試類
*/
public class TestAccount {
public static void main(String[] args) {
Accout a = new Accout();
StudentThread s = new StudentThread(a);
GenearchThread g = new GenearchThread(a);
}
}
package syn4;
/**
* 模擬學(xué)生線程
*/
public class StudentThread extends Thread {
Accout a;
public StudentThread(Accout a){
this.a = a;
start();
}
public void run(){
try{
while(true){
Thread.sleep(2000);
a.getMoney(); //取錢
}
}catch(Exception e){}
}
}
package syn4;
/**
* 家長線程
*/
public class GenearchThread extends Thread {
Accout a;
public GenearchThread(Accout a){
this.a = a;
start();
}
public void run(){
try{
while(true){
Thread.sleep(12000);
a.saveMoney(); //存錢
}
}catch(Exception e){}
}
}
package syn4;
/**
* 銀行賬戶
*/
public class Accout {
int money = 0;
/**
* 取錢
* 如果賬戶沒錢則等待,否則取出所有錢提醒存錢
*/
public synchronized void getMoney(){
System.out.println("準(zhǔn)備取錢!");
try{
if(money == 0){
wait(); //等待
}
//取所有錢
System.out.println("剩余:" + money);
money -= 50;
//提醒存錢
notify();
}catch(Exception e){}
}
/**
* 存錢
* 如果有錢則等待,否則存入200提醒取錢
*/
public synchronized void saveMoney(){
System.out.println("準(zhǔn)備存錢!");
try{
if(money != 0){
wait(); //等待
}
//取所有錢
money = 200;
System.out.println("存入:" + money);
//提醒存錢
notify();
}catch(Exception e){}
}
}
該程序的一部分執(zhí)行結(jié)果為:
準(zhǔn)備取錢!
準(zhǔn)備存錢!
存入:200
剩余:200
準(zhǔn)備取錢!
剩余:150
準(zhǔn)備取錢!
剩余:100
準(zhǔn)備取錢!
剩余:50
準(zhǔn)備取錢!
準(zhǔn)備存錢!
存入:200
剩余:200
準(zhǔn)備取錢!
剩余:150
準(zhǔn)備取錢!
剩余:100
準(zhǔn)備取錢!
剩余:50
準(zhǔn)備取錢!
在該示例代碼中,TestAccount類是測試類,主要實(shí)現(xiàn)創(chuàng)建帳戶Account類的對(duì)象,以及啟動(dòng)學(xué)生線程StudentThread和啟動(dòng)家長線程GenearchThread。在StudentThread線程中,執(zhí)行的功能是每隔2秒中取一次錢,每次取50元。在GenearchThread線程中,執(zhí)行的功能是每隔12秒存一次錢,每次存200。這樣存款和取款之間不僅時(shí)間間隔存在差異,而且數(shù)量上也會(huì)出現(xiàn)交叉。而該示例中,最核心的代碼是Account類的實(shí)現(xiàn)。
在Account類中,實(shí)現(xiàn)了同步控制功能,在該類中包含一個(gè)關(guān)鍵的屬性money,該屬性的作用是存儲(chǔ)帳戶金額。在介紹該類的實(shí)現(xiàn)前,首先介紹一下兩個(gè)同步方法——wait和notify方法的使用,這兩個(gè)方法都是Object類中的方法,也就是說每個(gè)類都包含這兩個(gè)方法,換句話說,就是Java天生就支持同步處理。這兩個(gè)方法都只能在synchronized修飾的方法或語句塊內(nèi)部采用被調(diào)用。其中wait方法的作用是使調(diào)用該方法的線程休眠,也就是使該線程退出CPU的等待隊(duì)列,處于冬眠狀態(tài),不執(zhí)行動(dòng)作,也不占用CPU排隊(duì)的時(shí)間,notify方法的作用是喚醒一個(gè)因?yàn)樵搶?duì)象的線程,該線程當(dāng)前處于休眠狀態(tài),至于喚醒的具體是那個(gè)則不保證。在Account類中,被StudentThread調(diào)用的getMoney方法的功能是判斷當(dāng)前金額是否是0,如果是則使StudentThread線程處于休眠狀態(tài),如果金額不是0,則取出50元,同時(shí)喚醒使用該帳戶對(duì)象的其它一個(gè)線程,而被GenearchThread線程調(diào)用的saveMoney方法的功能是判斷當(dāng)前是否不為0,如果是則使GenearchThread線程處于休眠狀態(tài),如果金額是0,則存入200元,同時(shí)喚醒使用該帳戶對(duì)象的其它一個(gè)線程。
如果還是不清楚,那就結(jié)合前面的程序執(zhí)行結(jié)果來解釋一下程序執(zhí)行的過程:在程序開始執(zhí)行時(shí),學(xué)生線程和家長線程都啟動(dòng)起來,所以輸出“準(zhǔn)備取錢”和“準(zhǔn)備存錢”,然后學(xué)生線程按照該線程run方法的邏輯執(zhí)行,先延遲2秒,然后調(diào)用帳戶對(duì)象a中的getMoney方法,但是由于初始情況下帳戶對(duì)象a中的money數(shù)值為0,所以學(xué)生線程就休眠了。在學(xué)生線程執(zhí)行的同時(shí),家長線程也按照該線程的run方法的邏輯執(zhí)行,先延遲12秒,然后調(diào)用帳戶對(duì)象a中的saveMoney方法,由于帳戶a對(duì)象中的money為零,條件不成立,所以執(zhí)行存入200元,同時(shí)喚醒線程,由于使用對(duì)象a的線程現(xiàn)在只有學(xué)生線程,所以學(xué)生線程被喚醒,開始執(zhí)行邏輯,取出50元,然后喚醒線程,由于當(dāng)前沒有線程處于休眠狀態(tài),所以沒有線程被喚醒。同時(shí)家長線程繼續(xù)執(zhí)行,先延遲12秒,這個(gè)時(shí)候?qū)W生線程執(zhí)行了4次,耗時(shí)4X2秒=8秒,就取光了帳戶中的錢,接著由于帳戶為0則學(xué)生線程又休眠了,一直到家長線程延遲12秒結(jié)束以后,判斷帳戶為0,又存入了200元,程序繼續(xù)執(zhí)行下去。
在解決多線程問題是,互斥和同步都是解決問題的思路,如果需要形象的比較這兩種方式的區(qū)別的話,就看一下下面的示例。一個(gè)比較忙的老總,桌子上有2部電話,在一部處于通話狀態(tài)時(shí),另一部響了,老總拿其這部電話說我在接電話,你等一下,而沒有掛電話,這種處理的方式就是互斥。而如果老總拿其另一部電話說,我在接電話,等會(huì)我打給你,然后掛了電話,這種處理的方式就是同步。兩者相比,互斥明顯占用系統(tǒng)資源(浪費(fèi)電話費(fèi),浪費(fèi)別人的時(shí)間),而同步則是一種更加好的解決問題的思路。
zhuan:http://blog.csdn.net/Mailbomb/archive/2009/05/27/4220846.aspx