Java語言內置了synchronized關鍵字用于對多線程進行同步,大大方便了Java中多線程程序的編寫。但是僅僅使用synchronized關鍵字還不能滿足對多線程進行同步的所有需要。大家知道,synchronized僅僅能夠對方法或者代碼塊進行同步,如果我們一個應用需要跨越多個方法進行同步,synchroinzed就不能勝任了。在C++中有很多同步機制,比如信號量、互斥體、臨屆區等。在Java中也可以在synchronized語言特性的基礎上,在更高層次構建這樣的同步工具,以方便我們的使用。
????當前,廣為使用的是由Doug?Lea編寫的一個Java中同步的工具包,可以在這兒了解更多這個包的詳細情況:
????http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html
????該工具包已經作為JSR166正處于JCP的控制下,即將作為JDK1.5的正式組成部分。本文并不打算詳細剖析這個工具包,而是對多種同步機制的一個介紹,同時給出這類同步機制的實例實現,這并不是工業級的實現。但其中會參考Doug?Lea的這個同步包中的工業級實現的一些代碼片斷。
????本例中還沿用上篇中的Account類,不過我們這兒編寫一個新的ATM類來模擬自動提款機,通過一個ATMTester的類,生成10個ATM線程,同時對John賬戶進行查詢、提款和存款操作。Account類做了一些改動,以便適應本篇的需要:
- import ?java.util.HashMap;
- import ?java.util.Map;
- class ?Account?{
- ????String?name;
- ????//float?amount;
- ????
- ????//使用一個Map模擬持久存儲
- ????static?Map?storage?=?new?HashMap();
- ????static?{
- ????????storage.put("John",?new?Float(1000.0f));
- ????????storage.put("Mike",?new?Float(800.0f));
- ????}????
- ????
- ????
- ????public?Account(String?name)?{
- ????????//System.out.println("new?account:"?+?name);
- ????????this.name?=?name;
- ????????//this.amount?=?((Float)storage.get(name)).floatValue();
- ????}
- ????public?synchronized?void?deposit(float?amt)?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????storage.put(name,?new?Float(amount?+?amt));
- ????}
- ????public?synchronized?void?withdraw(float?amt)?throws?InsufficientBalanceException?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????if?(amount?>=?amt)
- ????????????amount?-=?amt;
- ????????else?
- ????????????throw?new?InsufficientBalanceException();
- ????????????????
- ????????storage.put(name,?new?Float(amount));
- ????}
- ????public?float?getBalance()?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????return?amount;
- ????}
- }
在新的Account類中,我們采用一個HashMap來存儲賬戶信息。Account由ATM類通過login登錄后使用:
- public ?class?ATM?{
- ????Account?acc;
- ????
- ????//作為演示,省略了密碼驗證
- ????public?boolean?login(String?name)?{
- ????????if?(acc?!=?null)
- ????????????throw?new?IllegalArgumentException("Already?logged?in!");
- ????????acc?=?new?Account(name);
- ????????return?true;
- ????}
- ????
- ????public?void?deposit(float?amt)?{
- ????????acc.deposit(amt);
- ????}
- ????
- ????public?void?withdraw(float?amt)?throws?InsufficientBalanceException??{
- ????????????acc.withdraw(amt);
- ????}
- ????
- ????public?float?getBalance()?{
- ????????return?acc.getBalance();
- ????}
- ????
- ????public?void?logout?()?{
- ????????acc?=?null;
- ????}
- ????
- }
-
下面是ATMTester,在ATMTester中首先生成了10個ATM實例,然后啟動10個線程,同時登錄John的賬戶,先查詢余額,然后,再提取余額的80%,然后再存入等額的款(以維持最終的余額的不變)。按照我們的預想,應該不會發生金額不足的問題。首先看代碼:
- public ?class?ATMTester?{
- ????private?static?final?int?NUM_OF_ATM?=?10;
- ????public?static?void?main(String[]?args)?{
- ????????ATMTester?tester?=?new?ATMTester();
- ????????
- ????????final?Thread?thread[]?=?new?Thread[NUM_OF_ATM];
- ????????final?ATM?atm[]?=?new?ATM[NUM_OF_ATM];
- ????????for?(int?i=0;?i
- ????????????atm[i]?=?new?ATM();
- ????????????thread[i]?=?new?Thread(tester.new?Runner(atm[i]));
- ????????????thread[i].start();
- ????????}????
- ????????
- ????}
- ????
- ????class?Runner?implements?Runnable?{
- ????????ATM?atm;
- ????????
- ????????Runner(ATM?atm)?{
- ????????????this.atm?=?atm;
- ????????}
- ????????
- ????????public?void?run()?{
- ????????????atm.login("John");
- ????????????//查詢余額
- ????????????float?bal?=?atm.getBalance();
- ????????????try?{
- ????????????????Thread.sleep(1);?//模擬人從查詢到取款之間的間隔
- ????????????}?catch?(InterruptedException?e)?{
- ????????????????//?ignore?it
- ????????????}?
- ????????????
- ????????????try?{
- ????????????????System.out.println("Your?balance?is:"?+?bal);
- ????????????????System.out.println("withdraw:"?+?bal?*?0.8f);
- ????????????????atm.withdraw(bal?*?0.8f);
- ????????????????System.out.println("deposit:"?+?bal?*?0.8f);
- ????????????????atm.deposit(bal?*?0.8f);
- ????????????}?catch?(InsufficientBalanceException?e1)?{
- ????????????????System.out.println("余額不足!");
- ????????????}?finally?{
- ????????????????????????????????????atm.logout();
- ???????????????????????????}
- ????????}
- ????}
- }
運行ATMTester,結果如下(每次運行結果都有所差異):
Your?balance?is:1000.0
withdraw:800.0
deposit:800.0
Your?balance?is:1000.0
Your?balance?is:1000.0
withdraw:800.0
withdraw:800.0
余額不足!
Your?balance?is:200.0
Your?balance?is:200.0
Your?balance?is:200.0
余額不足!
Your?balance?is:200.0
Your?balance?is:200.0
Your?balance?is:200.0
Your?balance?is:200.0
withdraw:160.0
withdraw:160.0
withdraw:160.0
withdraw:160.0
withdraw:160.0
withdraw:160.0
withdraw:160.0
deposit:160.0
余額不足!
余額不足!
余額不足!
余額不足!
余額不足!
余額不足!
為什么會出現這樣的情況?因為我們這兒是多個ATM同時對同一賬戶進行操作,比如一個ATM查詢出了余額為1000,第二個ATM也查詢出了余額1000,然后兩者都期望提取出800,那么只有第1個用戶能夠成功提出,因為在第1個提出800后,賬戶真實的余額就只有200了,而第二個用戶仍認為余額為1000。這個問題是由于多個ATM同時對同一個賬戶進行操作所不可避免產生的后果。要解決這個問題,就必須限制同一個賬戶在某一時刻,只能由一個ATM進行操作。如何才能做到這一點?直接通過synchronized關鍵字可以嗎?非常遺憾!因為我們現在需要對整個Account的多個方法進行同步,這是跨越多個方法的,而synchronized僅能對方法或者代碼塊進行同步。在下一篇我們將通過編寫一個鎖對象達到這個目的。
我們首先開發一個BusyFlag的類,類似于C++中的Simaphore。
- public ?class?BusyFlag?{
- ????protected?Thread?busyflag?=?null;
- ????protected?int?busycount?=?0;
- ????
- ????public?synchronized?void?getBusyFlag()?{
- ????????while?(tryGetBusyFlag()?==?false)?{
- ????????????try?{
- ????????????????wait();
- ????????????}?catch?(Exception?e)?{}????????????
- ????????}
- ????}
- ????
- ????private?synchronized?boolean?tryGetBusyFlag()?{
- ????????if?(busyflag?==?null)?{
- ????????????busyflag?=?Thread.currentThread();
- ????????????busycount?=?1;
- ????????????return?true;
- ????????}
- ????????
- ????????if?(busyflag?==?Thread.currentThread())?{
- ????????????busycount++;
- ????????????return?true;
- ????????}
- ????????return?false;????????
- ????}
- ????
- ????public?synchronized?void?freeBusyFlag()?{
- ????????if(getOwner()==?Thread.currentThread())?{
- ????????????busycount--;
- ????????????if(busycount==0)?{
- ????????????????busyflag?=?null;
- ?????????????????????????????????????notify();
- ????????????????????????????}
- ????????}
- ????}
- ????
- ????public?synchronized?Thread?getOwner()?{
- ????????return?busyflag;
- ????}
- }
注:參考Scott?Oaks?&?Henry?Wong《Java?Thread》
BusyFlag有3個公開方法:getBusyFlag,?freeBusyFlag,?getOwner,分別用于獲取忙標志、釋放忙標志和獲取當前占用忙標志的線程。使用這個BusyFlag也非常地簡單,只需要在需要鎖定的地方,調用BusyFlag的getBusyFlag(),在對鎖定的資源使用完畢時,再調用改BusyFlag的freeBusyFlag()即可。下面我們開始改造上篇中的Account和ATM類,并應用BusyFlag工具類使得同時只有一個線程能夠訪問同一個賬戶的目標得以實現。首先,要改造Account類,在Account中內置了一個BusyFlag對象,并通過此標志對象對Account進行鎖定和解鎖:
- import ?java.util.Collections;
- import ?java.util.HashMap;
- import ?java.util.Map;
- class ?Account?{
- ????String?name;
- ????//float?amount;
- ????
- ????BusyFlag?flag?=?new?BusyFlag();
- ????
- ????//使用一個Map模擬持久存儲
- ????static?Map?storage?=?new?HashMap();
- ????static?{
- ????????storage.put("John",?new?Float(1000.0f));
- ????????storage.put("Mike",?new?Float(800.0f));
- ????}
- ????
- ????static?Map?accounts?=?Collections.synchronizedMap(new?HashMap());????
- ????
- ????
- ????private?Account(String?name)?{
- ????????this.name?=?name;
- ????????//this.amount?=?((Float)storage.get(name)).floatValue();
- ????}
- ????
- ????public?synchronized?static?Account?getAccount?(String?name)?{
- ????????if?(accounts.get(name)?==?null)
- ????????????accounts.put(name,?new?Account(name));
- ????????return?(Account)?accounts.get(name);
- ????}
- ????public?synchronized?void?deposit(float?amt)?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????storage.put(name,?new?Float(amount?+?amt));
- ????}
- ????public?synchronized?void?withdraw(float?amt)?throws?InsufficientBalanceException?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????if?(amount?>=?amt)
- ????????????amount?-=?amt;
- ????????else?
- ????????????throw?new?InsufficientBalanceException();
- ????????????????
- ????????storage.put(name,?new?Float(amount));
- ????}
- ????public?float?getBalance()?{
- ????????float?amount?=?((Float)storage.get(name)).floatValue();
- ????????return?amount;
- ????}
- ????
- ????public?void?lock()?{
- ????????flag.getBusyFlag();
- ????}
- ????
- ????public?void?unlock()?{
- ????????flag.freeBusyFlag();
- ????}
- }
新的Account提供了兩個用于鎖定的方法:lock()和unlock(),供Account對象的客戶端在需要時鎖定Account和解鎖Account,Account通過委托給BusyFlag來提供這個機制。另外,大家也發現了,新的Account中提供了對Account對象的緩存,同時去除了public的構造方法,改為使用一個靜態工廠方法供用戶獲取Account的實例,這樣做也是有必要的,因為我們希望所有的ATM機同時只能有一個能夠對同一個Account進行操作,我們在Account上的鎖定是對一個特定Account對象進行加鎖,如果多個ATM同時實例化多個同一個user的Account對象,那么仍然可以同時操作同一個賬戶。所以,要使用這種機制就必須保證Account對象在系統中的唯一性,所以,這兒使用一個Account的緩存,并將Account的構造方法變為私有的。你也可以說,通過在Account類鎖上進行同步,即將Account中的BusyFlag對象聲明為static的,但這樣就使同時只能有一臺ATM機進行操作了。這樣,在一臺ATM機在操作時,全市其它的所有的ATM機都必須等待。
另外必須注意的一點是:Account中的getAccount()方法必須同步,否則,將有可能生成多個Account對象,因為可能多個線程同時到達這個方法,并監測到accounts中沒有“John”的Account實例,從而實例化多個John的Account實例。s
ATM類只需作少量改動,在login方法中鎖定Account,在logout方法中解鎖:
- public ?class?ATM?{
- ????Account?acc;
- ????
- ????//作為演示,省略了密碼驗證
- ????public?synchronized?boolean?login(String?name)?{
- ????????if?(acc?!=?null)
- ????????????throw?new?IllegalArgumentException("Already?logged?in!");
- ????????acc?=?Account.getAccount(name);
- ????????acc.lock();
- ????????return?true;
- ????}
- ????
- ????public?void?deposit(float?amt)?{
- ????????acc.deposit(amt);
- ????}
- ????
- ????public?void?withdraw(float?amt)?throws?InsufficientBalanceException??{
- ????????????acc.withdraw(amt);
- ????}
- ????
- ????public?float?getBalance()?{
- ????????return?acc.getBalance();
- ????}
- ????
- ????public?synchronized?void?logout?()?{
- ????????acc.unlock();
- ????????acc?=?null;
- ????}
- ????
- }
ATMTester類不需要做任何修改即可同樣運行,同時保證同一個Account同時只能由一個ATM進行操作。解決了上篇提到的多個ATM同時對同一個Account進行操作造成的問題。
在最新的Doug?Lea的util.concurrent工具包中(現處于JSR166)提供了類似的并發實用類:ReentrantLock,它實現了java?.util.concurrent.locks.Lock接口(將在JDK1.5中發布),它的作用也類似于我們這兒的BusyFlag,實現機制、使用方法也相似。但這是一個工業強度的可重入鎖的實現類。在ReentrantLock的API文檔中有它的使用示例:
- ?????Lock?l?=?...;?
- ?????l.lock();
- ?????try?{
- ?????????//?access?the?resource?protected?by?this?lock
- ?????}?finally?{
- ?????????l.unlock();
- ?????}