Java(8-2)多線程的同步和條件對象

這一節,我們要一口氣介紹完多線程同步和條件對象的原因,鎖機制,條件對象的使用以及我們之前寫的那個銀行轉錢系統的剩餘代碼 。

Part 1 同步
爲什麼要有同步呢? 我們從之前多線程的一個例子來看:有很多用戶都分別在進行交易,假若其中一個線程,在正在進行存錢操作,但是還未結束的時候,它在CPU的時間片就已經用完了,這個線程被中斷了!而他還沒把存的錢寫入記錄!卻轉而去運行其他線程了,等下一次他的時間片到來時,之前關於存錢的更新在內存中已經被擦去,那麼,我們銀行系統的總賬戶餘額就會變少(你願意把錢存進這樣的銀行嘛? 反正我不願意),這顯然是我們不希望遇見的!

爲了防止這種情況出現,我們有兩種防止代碼受併發訪問的干擾:一種是在Java SE 5.0引入的ReetrantLock類; 一種是synchronized關鍵字,將自動提供一個鎖及其相關條件。

Part 1.1 鎖對象介紹

鎖對象的應用如下:

myLock.lock()// 一個鎖對象上鎖
try{
    //進行操作
}finally{
    myLock.unlock();// 在這裏對鎖對象進行解鎖
}

這一結構確保了任何時刻只有一個線程進入臨界區,這樣我們銀行之前存入的操作就不會被擦掉了。一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句。其他線程調用lock時,他們將被阻塞,知道第一個線程釋放所對象。

Tip:
如果兩個線程試圖放問同一個Bank對象(每個Bank都有一個鎖),那麼鎖將以串行的方式提供服務;但是,如果兩個線程訪問不同的Bank對象,每一個線程將得到不同的所對象,兩個線程都不會發生阻塞。

鎖是可重入的,因爲線程可以重複得獲得已經持有的鎖。鎖保持一個持有計數來跟蹤對lock方法的嵌套使用。線程每一次調用lock方法都會使計數增加,每次調用Unlock都會使計數減少,由於這一特性,被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法。

舉個例子,在我們最終的代碼中,transfer方法調用getTotalBalance方法,這也會封鎖bankLock對象,此時bankLock對象持有的鎖的計數爲2。當getTotalBalance方法退出的時候,持有計數變回1。當transfer方法退出時,持有計數變爲0,線程釋放鎖。

Part 1.2 條件對象
我們對程序上了鎖,這樣避免了一些令我們難過的情況,不過事實上,事情比我們往往比我們想的還要複雜,現在設想:銀行的一個線程,進入了臨界區,他想進行取錢操作,但是發現沒有足夠的錢可以取出(可能是工資還沒打到用戶的賬戶上),只有錢足夠時,才能去出錢,但是他現在又對本身的Bank對象上了鎖,因此別的線程無權對這個Bank對象進行操作,這時,我們就需要使用條件對象了 。

條件對象的使用如下:

    private Condition sufficientFunds;
    ...
    public Bank()
    {
        ...
        sufficientFunds = bankLock.newCondition();
    }
}

如果transfer方法發現餘額不足,他會調用:

sufficientFunds.await();

當前線程現在被阻塞了,並放棄了鎖。我們希望這樣可以使得另一個線程可以進行當前賬戶增加賬戶餘額的操作 。

等待獲得鎖的線程和調用await方法的線程存在本質上的不同。一旦一個線程調用await方法,他進入該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,他處於阻塞狀態,知道另一個線程調用同一個條件上的signalAll方法時爲止。

當一個線程轉賬時,它應該調用:sufficientFunds.signalAll()
singnAll這一調用重新激活因爲這一條件兒等待的所有進程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活他們。同時,他們將試圖重新進入該對象。一旦,鎖成爲可用的,他們中的某個將從await調用返回,獲得該鎖並從阻塞的地方繼續執行。
此時,線程應該再次測試該條件。由於無法確保該條件被滿足——signalAll方法僅僅是通知正在等待的線程:此時可能已經滿足條件,值得再次去檢測該條件。

至關重要的是最終需要某個其他線程調用signalAll方法。當一個線程調用await時,他沒有辦法重新激活自身。 只能寄希望於其他線程。如果沒有其他線程來重新激活等待的線程,他就永遠不會再運行了,這將導致令人不快的死鎖現象。

那我們應該何時調用signalAll方法開避免碰見阻塞呢?經驗上講,在對象的狀態有利於等待線程的方向改變時調用signalAll。 例如,在我們一會最終的程序中,將會存在:當一個用戶餘額發生改變時,等待的線程會應該有機會檢查餘額(當完成轉賬時,調用signaAll方法)。

還要注意,調用signalAll不會立即激活一個等待線程。它僅僅解除等待線程的阻塞,以便這些線程可以在當前線程退出同步方法之後,通過競爭實現對對象的訪問。

下面就是最終的銀行代碼:

public class BankTest{
    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_AMOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args)
    {
        Bank bank = new Bank(NACCOUNTS,INITIAL_BALANCE);
        for(int i = 0;i < NACCOUNTS;i++)
        {
            int fromAccount = i;
            Runnable r = new Runnable(){
                try{
                    while(true){
                        int toAccount = (int)(bank.size() * Math.random());
                        double amount = MAX_AMOUNT * Math.random();
                        bank.transfer(fromAccount,toAccount,amount);
                        Thread.sleep((int)(DELAY * Math.random()));
                    }
                }
                catch(InterruptedException e){}
            };
            Thread t = new Thread(r);
            t.start();
        }
    }
}

public class Bank(){
    private final double[] accounts;
    private Lock bankLock; //這裏申明瞭鎖對象引用
    private Condition sufficientFunds;//這裏申明瞭條件對象引用

    public Bank(int n,double initialBalance)
    {
        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        bankLock = new ReentrantLock();//構造方法中實例了鎖對象
        sufficientFunds = bankLock.newCondition()//構造方法中實例了條件對象
    }

    public void transfer(int from,int to,double amount)throws InterruptedException
    {
        bankLock.lock();
        try{
            while(accounts[from] < amount)
                sufficientFunds.await();
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf("%10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf("Total Balance:%10.2f%n",getTotalBalance);
            sufficientFunds.signaAll(); 
        }
        finally{
            bankLock.unlock();
            }
    }

    public double getTotalBalance()
    {
        bankLock.lock();
        try{
            double sum = 0;

            for(double e :; accounts)
                sum += a;
            return sum;
        }
        finally{
            bankLock.unlock();
        }
    }

    public int size(){
        return accounts.length;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章