java 多線程(三)條件對象

來源:互聯網
上載者:User

標籤:java   多線程   線程   

轉載請註明出處:http://blog.csdn.net/xingjiarong/article/details/47417383
在上一篇部落格中,我們學會了用ReentrantLock來控制線程訪問同一個資料,防止出現Race Condition。這一次呢,我們繼續深入的學習,學習一下java中的條件對象,條件對象在多線程同步中用到的比較多。

首先,我們來介紹一下臨界區。

臨界區:在同步的程式設計中,臨界區指的是一個訪問共用資源的程式片段,而這些共用資源又具有無法同時被多個線程訪問的特性。 當有線程進入臨界區時,其他線程或是進程必須等待,在一些情況下必須在臨界區的進入點與離開點採用一些特殊的方法,以確保這些共用資源是被互斥使用的。

現在我們來看一個新的例子,這是一個銀行轉賬的例子。在Bank類中,我們聲明了一個10個大小的數組,用來表示銀行中的10個賬戶,而對應的數值就是這個賬戶中對應的金額。Bank類提供了轉移賬戶資金的方法,可以從from賬戶,轉移amount的資金到to賬戶。還提供了獲得這些賬戶的總金額的方法,getTotalBalabce(),因為資金轉移是發生在這10個賬戶中的,所以無論怎樣轉移,總的金額應該是不變的。

Bank.java

import java.util.concurrent.locks.ReentrantLock;public class Bank {    /**     * 利用數組類比銀行賬戶     */    private final double accounts[];    private ReentrantLock lock = new ReentrantLock();    public Bank() {        accounts = new double[10];        /*         * 初始化,使每個賬戶都初始有initialBalance金錢         */        for (int i = 0; i < 10; i++)            accounts[i] = 1000;    }    /**     * 資金轉移的方法     *      * @param from     *            源賬戶     * @param to     *            目標賬戶     * @param amount     *            轉移金額     */    public void transfer(int from, int to, double amount) {        lock.lock();        try {            if (accounts[from] < amount)                return            System.out.print(Thread.currentThread());            accounts[from] -= amount;            System.out.printf("%5.2f from %d to %d", amount, from, to);            accounts[to] += amount;            System.out.printf(" Total Balance:%5.2f\n", getTotalBalance());        } catch (Exception e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    /**     * 擷取所有賬戶的總金額     *      * @return     */    public double getTotalBalance() {        lock.lock();        double sum = 0;        try {            for (double a : accounts)                sum += a;        } finally {            lock.unlock();        }        return sum;    }    /**     * 擷取現在賬戶的數量     *      * @return     */    public int size() {        return accounts.length;    }}

TransferRunnable這個類實現了Runnable介面,我們在Main函數中拿他做線程,它的run方法就是產生一個隨機的目標賬戶和轉移的金額,然後調用Bank類的方法將資金轉移到目標賬戶中去。

TransferRunnable.java

public class TransferRunnable implements Runnable{    private Bank bank;    private int fromAccount;    public TransferRunnable(Bank b,int from){        bank=b;        fromAccount=from;    }    public void run(){        try{            while(true)            {                int toAccount = (int)(bank.size()*Math.random());                double amount = 1000*Math.random();                bank.transfer(fromAccount, toAccount, amount);                Thread.sleep(1000);            }        }catch(Exception e){            e.printStackTrace();        }    }   }

在主程式中,我們建立了10個線程,每一個線程都從一個對應的賬戶轉移一定的資金到隨機的賬戶中去。
Main.java

public class Main {    public static void main(String[] args) {        Bank b = new Bank();        Thread[] threadArray = new Thread[10];        for (int i = 0; i < 10; i++) {            threadArray[i] = new Thread(new TransferRunnable(b, i));        }        for(int i=0;i<10;i++){            threadArray[i].start();        }    }}

在這個程式中有共同的變數——10個銀行賬戶的資金,所以就有對應的臨界區——Bank類的transfer方法和getTotalBalabce方法,如果不對臨界區加以保護的話,會導致多個線程同時進入臨界區,而導致結果錯誤,讀者可以實驗一下,將ReentrantLock加鎖的地方都去掉,看看總金額是不是會產生變化,在我的實驗中,不但總金額的數量不對,就連列印的語句順序都是不對的,說明一個線程在System.out.printf()的過程中都被別的線程打斷了。

看了這個例子,相信大家對ReentrantLock的使用更加熟練了。

現在我們來考慮這樣一個問題,大家看到transfer方法中,如果賬戶中的金額不足的時候就立刻返回而不再發生資金轉移,我現在不想這樣處理了。我想要這樣處理,如果賬戶資金不足的話,就一直等待,直到其他的賬戶向這個賬戶轉移足夠的資金了,然後再發生資金轉移。

問題是,操作這個賬戶的線程已經獲得了lock,其他的線程無法再進入lock方法,也就不可能向這個賬戶轉移相應的資金了,所以說,這個線程必須放棄lock的控制權,讓其他的線程獲得。

Java中提供了條件對象,Condition類,來配合ReentrantLock實現對臨界區的控制,我們對Bank類做下面的改變。

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class Bank {    /**     * 利用數組類比銀行賬戶     */    private final double accounts[];    private ReentrantLock lock = new ReentrantLock();    private Condition condition;    public Bank() {        accounts = new double[10];        /*         * 初始化,使每個賬戶都初始有initialBalance金錢         */        for (int i = 0; i < 10; i++)            accounts[i] = 1000;        condition = lock.newCondition();    }    /**     * 資金轉移的方法     *      * @param from     *            源賬戶     * @param to     *            目標賬戶     * @param amount     *            轉移金額     */    public void transfer(int from, int to, double amount) {        lock.lock();        try {            while (accounts[from] < amount)                condition.await();            System.out.print(Thread.currentThread());            accounts[from] -= amount;            System.out.printf("%5.2f from %d to %d", amount, from, to);            accounts[to] += amount;            System.out.printf(" Total Balance:%5.2f\n", getTotalBalance());            condition.signalAll();        } catch (Exception e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }    /**     * 擷取所有賬戶的總金額     *      * @return     */    public double getTotalBalance() {        lock.lock();        double sum = 0;        try {            for (double a : accounts)                sum += a;        } finally {            lock.unlock();        }        return sum;    }    /**     * 擷取現在賬戶的數量     *      * @return     */    public int size() {        return accounts.length;    }}

首先,聲明了一個Condition類的對象引用變數condition,在Bank的構造方法中進行的初始化,初始化是是調用的ReentrantLock類的方法newCondition(),說明Condition對象時依賴於ReentrantLock對象的,一個ReentrantLock對象可以建立多個條件對象,名字通常以控制的條件來命名。

然後在transfer方法中,下面兩行是關鍵:

while (accounts[from] < amount)    condition.await();

用while迴圈檢測賬戶的餘額,如果餘額不足就調用await()方法,await()會阻塞線程,並釋放線程保持的鎖,這時其他的線程就可以進入臨界區了,當賬戶的餘額滿足條件時,等待的線程也不會主動的喚醒,直到有一個線程調用了signalAll()方法。signalAll()方法喚醒所有因為不滿足條件而等待的線程,但是線程不一定能夠繼續向下執行,因為signalAll()喚醒線程時,並不是告訴線程你的條件滿足了,可以繼續向下執行了,而是告訴線程,現在條件改變,你可以重新檢測條件是否滿足了。如果條件被滿足,那麼也只有一個線程能夠繼續向下執行,因為一旦一個對象獲得了臨界區的鎖,其他的線程就不能再進入臨界區了。那麼應當什麼時候調用signalAll()方法呢,從經驗上將應該在對象的狀態有利於等待線程的方向改變時調用。這裡我們再發生資金轉移後調用,因為資金轉移後,其他的線程有可能滿足條件了。

注意:通常,await()的調用應該在如下形式的迴圈體中:

while(!(ok to proceed))    condition.await();

這裡的while迴圈不能換成if條件陳述式,因為被別的線程用signalAll方法喚醒的線程,僅僅是條件可能滿足了,而不是條件一定滿足了,如果不用while迴圈繼續檢測的話,就會造成條件不滿足的線程繼續向下執行,從而產生錯誤。

當一個線程擁有某個條件的鎖時,它僅僅可以在該條件上調用await(),signalAll(),signal()這三個方法,現在我們講講第三個方法。

signal和signalAll的區別是,signal不會將全部的等待線程喚醒,而是隨機播放一個線程喚醒,而signalAll是將全部的等待線程都喚醒。

最後附上源碼:http://download.csdn.net/detail/xingjiarong/9010675

在下一篇部落格裡,我會為大家介紹java的synchronized關鍵字,希望與大家一起學習一起進步,請大家繼續關注我的部落格,如果大家支援我的話,就頂我一下吧。

著作權聲明:本文為博主原創文章,轉載請註明出處,查看原文章,請訪問:http://blog.csdn.net/xingjiarong

java 多線程(三)條件對象

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.