如果這個例子還不能幫助你理解如何解決多線程的問題,那麼下面再來看一個更加實際的例子——衛生間問題.
例如火車上車廂的衛生間,為了簡單,這裡只模擬一個衛生間,這個衛生間會被多個人同時使用,在實際使用時,當一個人進入衛生間時則會把衛生間鎖上,等出來時打開門,下一個人進去把門鎖上,如果有一個人在衛生間內部則別人的人發現門是鎖的則只能在外面等待.從編程的角度來看,這裡的每個人都可以看作是一個線程對象,而這個衛生間對象由於被多個線程訪問,則就是臨界資源,在一個線程實際使用時,使用synchronized關鍵將臨界資源鎖定,當結束時,釋放鎖定.實現的代碼如下:
package syn3; /** * 測試類 */ public class TestHuman { public static void main(String[] args) { Toilet t = new Toilet(); //衛生間對象 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(); //啟動線程 }
public void run(){ //進入衛生間 t.enter(name); } } package syn3; /** * 衛生間,互斥的演示 */ public class Toilet { public synchronized void enter(String name){ System.out.println(name "已進入!"); try{ Thread.sleep(2000); }catch(Exception e){} System.out.println(name "離開!"); } } |
該示例的執行結果為,不同次數下執行結果會有所不同:
1已進入! 1離開! 3已進入! 3離開! 2已進入! 2離開! |
在該示例代碼中,Toilet類表示衛生間類,Human類模擬人,是該示例中的線程類,TestHuman類是測試類,用於啟動線程.在TestHuman中,創建一個Toilet類型的對象t,並將該對象傳遞到後續創建的線程對象中,這樣後續的線程對象就使用同一個Toilet對象,該對象就成為了臨界資源.下面創建了三個Human類型的線程對象,每個線程具有自己的名稱name參數,模擬3個線程,在每個線程對象中,只是調用對象t中的enter方法,模擬進入衛生間的動作,在enter方法中,在進入時輸出調用該方法的線程進入,然後延遲2秒,輸出該線程離開,然後後續的一個線程進入,直到三個線程都完成enter方法則程序結束.
在該示例中,同一個Toilet類的對象t的enter方法由於具有synchronized修飾符修飾,則在多個線程同時調用該方法時,如果一個線程進入到enter方法內部,則為對象t上鎖,直到enter方法結束以後釋放對該對象的鎖定,通過這種方式實現無論多少個Human類型的線程,對於同一個對象t,任何時候只能有一個線程執行enter方法,這就是解決多線程問題的第一種思路——互斥的解決原理.
12.4.2 同步
使用互斥解決多線程問題是一種簡單有效的解決辦法,但是由於該方法比較簡單,只能解決一些基本的問題,對於複雜的問題就無法解決了.
解決多線程問題的另外一種思路是同步.同步是另外一種解決問題的思路,結合前面衛生間的示例,互斥方式解決多線程的原理是,當一個人進入到衛生間內部時,別的人只能在外部時刻等待,這樣就相當於別的人雖然沒有事情做,但是還是要佔用別的人的時間,浪費系統的執行資源.而同步解決問題的原理是,如果一個人進入到衛生間內部時,則別的人可以去睡覺,不佔用系統資源,而當這個人從衛生間出來以後,把這個睡覺的人叫醒,則它就可以使用臨界資源了.使用同步的思路解決多線程問題更加有效,更加節約系統的資源.
在常見的多線程問題解決中,同步問題的典型示例是「生產者-消費者」模型,也就是生產者線程只負責生產,消費者線程只負責消費,在消費者發現無內容可消費時則睡覺.下面舉一個比較實際的例子——生活費問題.
生活費問題是這樣的:學生每月都需要生活費,家長一次預存一段時間的生活費,家長和學生使用統一的一個帳號,在學生每次取帳號中一部分錢,直到帳號中沒錢時通知家長存錢,而家長看到帳戶還有錢則不存錢,直到帳戶沒錢時才存錢.在這個例子中,這個帳號被學生和家長兩個線程同時訪問,則帳號就是臨界資源,兩個線程是同時執行的,當每個線程發現不符合要求時則等待,並釋放分配給自己的CPU執行時間,也就是不佔用系統資源.實現該示例的代碼為:
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; /** * 模擬學生線程 */ 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("準備取錢!"); try{ if(money == 0){ wait(); //等待 } //取所有錢 System.out.println("剩餘:" money); money -= 50; //提醒存錢 notify(); }catch(Exception e){} }
/** * 存錢 * 如果有錢則等待,否則存入200提醒取錢 */ public synchronized void saveMoney(){ System.out.println("準備存錢!"); try{ if(money != 0){ wait(); //等待 } //取所有錢 money = 200; System.out.println("存入:" money); //提醒存錢 notify(); }catch(Exception e){} } } |
該程序的一部分執行結果為:
準備取錢! 準備存錢! 存入:200 剩餘:200 準備取錢! 剩餘:150 準備取錢! 剩餘:100 準備取錢! 剩餘:50 準備取錢! 準備存錢! 存入:200 剩餘:200 準備取錢! 剩餘:150 準備取錢! 剩餘:100 準備取錢! 剩餘:50 準備取錢! |
在該示例代碼中,TestAccount類是測試類,主要實現創建帳戶Account類的對象,以及啟動學生線程StudentThread和啟動家長線程GenearchThread.在StudentThread線程中,執行的功能是每隔2秒中取一次錢,每次取50元.在GenearchThread線程中,執行的功能是每隔12秒存一次錢,每次存200.這樣存款和取款之間不僅時間間隔存在差異,數量上也會出現交叉.而該示例中,最核心的代碼是Account類的實現.
在Account類中,實現了同步控制功能,在該類中包含一個關鍵的屬性money,該屬性的作用是存儲帳戶金額.在介紹該類的實現前,介紹一下兩個同步方法——wait和notify方法的使用,這兩個方法都是Object類中的方法,也就是說每個類都包含這兩個方法,換句話說,就是Java天生就支持同步處理.這兩個方法都只能在synchronized修飾的方法或語句塊內部採用被調用.其中wait方法的作用是使調用該方法的線程休眠,也就是使該線程退出CPU的等待隊列,處於冬眠狀態,不執行動作,也不佔用CPU排隊的時間,notify方法的作用是喚醒一個該對象的線程,該線程當前處於休眠狀態,至於喚醒的具體是那個則不保證.在Account類中,被StudentThread調用的getMoney方法的功能是判斷當前金額是否是0,如果是則使StudentThread線程處於休眠狀態,如果金額不是0,則取出50元,同時喚醒使用該帳戶對象的其它一個線程,而被GenearchThread線程調用的saveMoney方法的功能是判斷當前是否不為0,如果是則使GenearchThread線程處於休眠狀態,如果金額是0,則存入200元,同時喚醒使用該帳戶對象的其它一個線程.
如果還是不清楚,那就結合前面的程序執行結果來解釋一下程序執行的過程:在程序開始執行時,學生線程和家長線程都啟動起來,輸出「準備取錢」和「準備存錢」,然後學生線程按照該線程run方法的邏輯執行,先延遲2秒,然後調用帳戶對象a中的getMoney方法,但是由於初始情況下帳戶對象a中的money數值為0,學生線程就休眠了.在學生線程執行的同時,家長線程也按照該線程的run方法的邏輯執行,先延遲12秒,然後調用帳戶對象a中的saveMoney方法,由於帳戶a對象中的money為零,條件不成立,執行存入200元,同時喚醒線程,由於使用對象a的線程現在只有學生線程,學生線程被喚醒,開始執行邏輯,取出50元,然後喚醒線程,由於當前沒有線程處於休眠狀態,沒有線程被喚醒.同時家長線程繼續執行,先延遲12秒,這個時候學生線程執行了4次,耗時4X2秒=8秒,就取光了帳戶中的錢,接著由於帳戶為0則學生線程又休眠了,一直到家長線程延遲12秒結束以後,判斷帳戶為0,又存入了200元,程序繼續執行下去.
在解決多線程問題是,互斥和同步都是解決問題的思路,如果需要形象的比較這兩種方式的區別的話,就看一下下面的示例.一個比較忙的老總,桌子上有2部電話,在一部處於通話狀態時,另一部響了,老總拿其這部電話說我在接電話,你等一下,而沒有掛電話,這種處理的方式就是互斥.而如果老總拿其另一部電話說,我在接電話,等會我打給你,然後掛了電話,這種處理的方式就是同步.兩者相比,互斥明顯佔用系統資源(浪費電話費,浪費別人的時間),而同步則是一種更加好的解決問題的思路.
[火星人 ] Java編程那些事兒99——多線程問題及處理2已經有456次圍觀