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<NUM_OF_ATM; 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機都必須等待。