溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java并發(fā)編程中如何通過ReentrantLock和Condition實現(xiàn)銀行存取款

發(fā)布時間:2021-11-20 15:15:13 來源:億速云 閱讀:155 作者:柒染 欄目:云計算

本篇文章為大家展示了java并發(fā)編程中如何通過ReentrantLock和Condition實現(xiàn)銀行存取款,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

java.util.concurrent.locks包為鎖和等待條件提供一個框架的接口和類,它不同于內(nèi)置同步和監(jiān)視器。該框架允許更靈活地使用鎖和條件,但以更難用的語法為代價。 

        Lock 接口支持那些語義不同(重入、公平等)的鎖規(guī)則,可以在非阻塞式結(jié)構(gòu)的上下文(包括 hand-over-hand 和鎖重排算法)中使用這些規(guī)則。主要的實現(xiàn)是 ReentrantLock。 

        ReadWriteLock 接口以類似方式定義了一些讀取者可以共享而寫入者獨占的鎖。此包只提供了一個實現(xiàn),即 ReentrantReadWriteLock,因為它適用于大部分的標準用法上下文。但程序員可以創(chuàng)建自己的、適用于非標準要求的實現(xiàn)。 

   以下是locks包的相關(guān)類圖:

java并發(fā)編程中如何通過ReentrantLock和Condition實現(xiàn)銀行存取款

        在之前我們同步一段代碼或者對象時都是使用 synchronized關(guān)鍵字,使用的是Java語言的內(nèi)置特性,然而 synchronized的特性也導致了很多場景下出現(xiàn)問題,比如:

        在一段同步資源上,首先線程A獲得了該資源的鎖,并開始執(zhí)行,此時其他想要操作此資源的線程就必須等待。如果線程A因為某些原因而處于長時間操作的狀態(tài),比如等待網(wǎng)絡(luò),反復重試等等。那么其他線程就沒有辦法及時的處理它們的任務(wù),只能無限制的等待下去。如果線程A的鎖在持有一段時間后可自動被釋放,那么其他線程不就可以使用該資源了嗎?再有就是類似于數(shù)據(jù)庫中的共享鎖與排它鎖,是否也可以應(yīng)用到應(yīng)用程序中?所以引入Lock機制就可以很好的解決這些問題。

  Lock提供了比 synchronized更多的功能。但是要注意以下幾點:

  ? Lock不是Java語言內(nèi)置的,synchronized是Java語言的關(guān)鍵字,因此是內(nèi)置特性。Lock是一個類,通過這個類可以實現(xiàn)同步訪問;

  ? Lock和synchronized有一點非常大的不同,采用 synchronized不需要用戶去手動釋放鎖,當synchronized方法或者 synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而 Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。

一、可重入鎖 ReentrantLock

  想到鎖我們一般想到的是同步鎖即 Synchronized,這里介紹的可重入鎖ReentrantLock的效率更高。IBM對于可重入鎖進行了一個介紹:JDK 5.0 中更靈活、更具可伸縮性的鎖定機制

  這里簡單介紹下可重入鎖的分類:(假設(shè)線程A獲取了鎖,現(xiàn)在A執(zhí)行完成了,釋放了鎖同時喚醒了正在等待被喚醒的線程B。但是,A執(zhí)行喚醒操作到B真正獲取鎖的時間里可能存在線程C已經(jīng)獲取了鎖,造成正在排隊等待的B無法獲得鎖)

  1) 公平鎖: 

     由于B先在等待被喚醒,為了保證公平性原則,公平鎖會先讓B獲得鎖。

  2) 非公平鎖

     不保證B先獲取到鎖對象。

  這兩種鎖只要在構(gòu)造ReentrantLock對象時加以區(qū)分就可以了,當參數(shù)設(shè)置為true時為公平鎖,false時為非公平鎖,同時默認構(gòu)造函數(shù)也是創(chuàng)建了一個非公平鎖。

    private Lock lock = new ReentrantLock(true); ReentrantLock的公平鎖在性能和實效性上作了很大的犧牲,可以參考IBM上發(fā)的那篇文章中的說明。

二、條件變量 Condition

  Condition是java.util.concurrent.locks包下的一個接口,  Condition 接口描述了可能會與鎖有關(guān)聯(lián)的條件變量。這些變量在用法上與使用 Object.wait 訪問的隱式監(jiān)視器類似,但提供了更強大的功能。需要特別指出的是,單個 Lock 可能與多個 Condition 對象關(guān)聯(lián)。為了避免兼容性問題,Condition 方法的名稱與對應(yīng)的 Object 版本中的不同。 

       Condition 將 Object 監(jiān)視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現(xiàn)組合使用,為每個對象提供多個等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監(jiān)視器方法的使用。 

   Condition(也稱為條件隊列 或條件變量)為線程提供了一種手段,在某個狀態(tài)條件下直到接到另一個線程的通知,一直處于掛起狀態(tài)(即“等待”)。因為訪問此共享狀態(tài)信息發(fā)生在不同的線程中,所以它必須受到保護,因此要將某種形式的鎖與 Condition相關(guān)聯(lián)。

        Condition 實例實質(zhì)上被綁定到一個鎖上。

  這里不再對Locks包下的源碼進行分析。

三、ReentrantLock和Condition設(shè)計多線程存取款

1. 存款的時候,不能有線程在取款 。取款的時候,不能有線程在存款。

2. 取款時,余額大于取款金額才能進行取款操作,否則提示余額不足。

3.  當取款時,如果金額不足,則阻塞當前線程,并等待2s(可能有其他線程將錢存入)。

    如果2s之內(nèi)沒有其它線程完成存款,或者還是金額不足則打印金額不足。

    如果其它存入足夠金額則通知該阻塞線程,并完成取款操作。

/**
 * 普通銀行賬戶,不可透支
 */
public class MyCount {
    private String oid; // 賬號
    private int cash;   // 賬戶余額
    //賬戶鎖,這里采用公平鎖,掛起的取款線程優(yōu)先獲得鎖,而不是讓其它存取款線程獲得鎖
    private Lock lock = new ReentrantLock(true);
    private Condition _save = lock.newCondition(); // 存款條件
    private Condition _draw = lock.newCondition(); // 取款條件

    MyCount(String oid, int cash) {
        this.oid = oid;
        this.cash = cash;
    }

    /**
     * 存款
     * @param x 操作金額
     * @param name 操作人
     */
    public void saving(int x, String name) {
        lock.lock(); // 獲取鎖
        if (x > 0) {
            cash += x; // 存款
            System.out.println(name + "存款" + x + ",當前余額為" + cash);
        }
        _draw.signalAll(); // 喚醒所有等待線程。
        lock.unlock(); // 釋放鎖
    }

    /**
     * 取款
     * @param x  操作金額
     * @param name 操作人
     */
    public void drawing(int x, String name) {
        lock.lock(); // 獲取鎖
        try {
            if (cash - x < 0) {
                System.out.println(name + "阻塞中");
                _draw.await(2000,TimeUnit.MILLISECONDS); // 阻塞取款操作, await之后就隱示自動釋放了lock,直到被喚醒自動獲取
            }
            if(cash-x>=0){
                cash -= x; // 取款
                System.out.println(name + "取款" + x + ",當前余額為" + cash);
            }else{
                System.out.println(name+" 余額不足,當前余額為 "+cash+"   取款金額為 "+x);
            }
            // 喚醒所有存款操作,這里并沒有什么實際作用,因為存款代碼中沒有阻塞的操作
            _save.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 釋放鎖
        }
    }
}

這里的可重入鎖也可以設(shè)置成非公平鎖,這樣阻塞取款線程可能后與其它存取款操作。

/**
     * 存款線程類
     */
    static class SaveThread extends Thread {
        private String name; // 操作人
        private MyCount myCount; // 賬戶
        private int x; // 存款金額

        SaveThread(String name, MyCount myCount, int x) {
            this.name = name;
            this.myCount = myCount;
            this.x = x;
        }

        public void run() {
            myCount.saving(x, name);
        }
    }

    /**
     * 取款線程類
     */
    static class DrawThread extends Thread {
        private String name; // 操作人
        private MyCount myCount; // 賬戶
        private int x; // 存款金額

        DrawThread(String name, MyCount myCount, int x) {
            this.name = name;
            this.myCount = myCount;
            this.x = x;
        }

        public void run() {
            myCount.drawing(x, name);
        }
    }

    public static void main(String[] args) {
        // 創(chuàng)建并發(fā)訪問的賬戶
        MyCount myCount = new MyCount("95599200901215522", 1000);
        // 創(chuàng)建一個線程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        Thread t1 = new SaveThread("S1", myCount, 100);
        Thread t2 = new SaveThread("S2", myCount, 1000);
        Thread t3 = new DrawThread("D1", myCount, 12600);
        Thread t4 = new SaveThread("S3", myCount, 600);
        Thread t5 = new DrawThread("D2", myCount, 2300);
        Thread t6 = new DrawThread("D3", myCount, 1800);
        Thread t7 = new SaveThread("S4", myCount, 200);
        // 執(zhí)行各個線程
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        pool.execute(t6);
        pool.execute(t7);

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 關(guān)閉線程池
        pool.shutdown();
    }
}

上述類中定義了多個存取款的線程,執(zhí)行結(jié)果如下:

S1存款100,當前余額為1100
S3存款600,當前余額為1700
D2阻塞中
S2存款1000,當前余額為2700
D2取款2300,當前余額為400
D3阻塞中
S4存款200,當前余額為600
D3 余額不足,當前余額為 600   取款金額為 1800
D1阻塞中
D1 余額不足,當前余額為 600   取款金額為 12600

執(zhí)行步驟如下:

  1. 初始化賬戶,有余額100。

  2. S1,S3完成存款。

  3. D2取款,余額不足,釋放鎖并阻塞線程,進入等待隊列中。

  4.  S2完成存款操作后,會喚醒掛起的線程,這時D2完成了取款。

  5.  D3取款,余額不足,釋放鎖并阻塞線程,進入等待隊列中。

  6.  S4完成存款操作后,喚醒D3,但是依然余額不足,D3 取款失敗。

  7.  D1 進行取款,等待2s鐘,無任何線程將其喚醒,取款失敗。

這里需要注意的是,當Condition調(diào)用await()方法時,當前線程會釋放鎖(否則就和Sychnize就沒有區(qū)別了)

將銀行賬戶中的 鎖改成非公平鎖時,執(zhí)行的結(jié)果如下:

1存款100,當前余額為1100
S3存款600,當前余額為1700
D2阻塞中
S2存款1000,當前余額為2700
D3取款1800,當前余額為900
D2 余額不足,當前余額為 900   取款金額為 2300
S4存款200,當前余額為1100
D1阻塞中
D1 余額不足,當前余額為 1100   取款金額為 12600

D2 取款出現(xiàn)余額不足后釋放鎖,進入等待狀態(tài)。但是當S2線程完成存款后并沒有立刻執(zhí)行D2線程,而是被D3插隊了。

通過執(zhí)行結(jié)果可以看出 公平鎖和非公平鎖的區(qū)別,公平鎖能保證等待線程優(yōu)先執(zhí)行,但是非公平鎖可能會被其它線程插隊。

四、ArrayBlockingQueue中關(guān)于ReentrantLock和Condition的應(yīng)用

JDK源碼中關(guān)于可重入鎖的非常典型的應(yīng)用是 BlockingQueue,從它的源碼中的成員變量大概就能知道了(ArrayBlockingQueue為例):

/** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
// 主要解決多線程訪問的線程安全性問題
    final ReentrantLock lock;

    /** Condition for waiting takes */
 // 添加元素時,通過notEmpty 喚醒消費線程(在等待該條件)
    private final Condition notEmpty;

    /** Condition for waiting puts */
 // 刪除元素時,通過 notFull 喚醒生成線程(在等待該條件)
    private final Condition notFull;

ArrayBlockingQueue 是一個典型的生產(chǎn)者消費者模型,通過一個數(shù)組保存元素。為了保證添加和刪除元素的線程安全性,增加了可重入鎖和條件變量。

可重入鎖主要保證多線程對阻塞隊列的操作是線程安全的,同時為了讓被阻塞的消費者或者生產(chǎn)者能夠被自動喚醒,這里引入了條件變量。

java并發(fā)編程中如何通過ReentrantLock和Condition實現(xiàn)銀行存取款

當隊列已滿時,Producer會被阻塞,此時如果Customer消費一個元素時,被阻塞的Producer就會被自動喚醒并往隊列中添加元素。

上面的兩個例子可見java.util.concurrent.locks包下的ReentrantLock和Condition配合起來的靈活性及實用性。

上述內(nèi)容就是java并發(fā)編程中如何通過ReentrantLock和Condition實現(xiàn)銀行存取款,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI