1. 程式人生 > >java並發編程——通過ReentrantLock,Condition實現銀行存取款

java並發編程——通過ReentrantLock,Condition實現銀行存取款

分類 cond 時間 -s str execute 兼容性問題 AS round

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

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

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

  以下是locks包的相關類圖:

技術分享圖片

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

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

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

  ? Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;

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

一、可重入鎖 ReentrantLock

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

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

  1) 公平鎖:

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

  2) 非公平鎖

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

  這兩種鎖只要在構造ReentrantLock對象時加以區分就可以了,當參數設置為true時為公平鎖,false時為非公平鎖,同時默認構造函數也是創建了一個非公平鎖。

    private Lock lock = new ReentrantLock(true);

ReentrantLock的公平鎖在性能和實效性上作了很大的犧牲,可以參考IBM上發的那篇文章中的說明。

二、條件變量 Condition

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

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

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

Condition 實例實質上被綁定到一個鎖上。

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

三、ReentrantLock和Condition設計多線程存取款

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

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

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

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

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

/**
 * 普通銀行賬戶,不可透支
 */
public class MyCount {
    private String oid; // 賬號
    private int cash;   // 賬戶余額
    //賬戶鎖,這裏采用公平鎖,掛起的取款線程優先獲得鎖,而不是讓其它存取款線程獲得鎖
    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(); // 釋放鎖
        }
    }
}

這裏的可重入鎖也可以設置成非公平鎖,這樣阻塞取款線程可能後與其它存取款操作。

 /**
     * 存款線程類
     */
    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) {
        // 創建並發訪問的賬戶
        MyCount myCount = new MyCount("95599200901215522", 1000);
        // 創建一個線程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        Thread t1 = new SaveThread("張三", myCount, 100);
        Thread t2 = new SaveThread("李四", myCount, 1000);
        Thread t3 = new DrawThread("王五", myCount, 12600);
        Thread t4 = new SaveThread("老張", myCount, 600);
        Thread t5 = new DrawThread("老牛", myCount, 2300);
        Thread t6 = new DrawThread("胖子", myCount, 1800);
        Thread t7 = new SaveThread("測試", myCount, 200);
        // 執行各個線程
        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();
        }
        // 關閉線程池
        pool.shutdown();
    }
}

上述類中定義了多個存取款的線程,執行結果如下:

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

執行步驟如下:

  1. 初始化賬戶,有余額100。
  2. S1,S3完成存款。
  3. D2取款,余額不足,釋放鎖並阻塞線程,進入等待隊列中。
  4. S2完成存款操作後,會喚醒掛起的線程,這時D2完成了取款。
  5. D3取款,余額不足,釋放鎖並阻塞線程,進入等待隊列中。
  6. S4完成存款操作後,喚醒D3,但是依然余額不足,D3 取款失敗。
  7. D1 進行取款,等待2s鐘,無任何線程將其喚醒,取款失敗。

這裏需要註意的是,當Condition調用await()方法時,當前線程會釋放鎖(否則就和Sychnize就沒有區別了)

將銀行賬戶中的 鎖改成非公平鎖時,執行的結果如下:

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

D2 取款出現余額不足後釋放鎖,進入等待狀態。但是當S2線程完成存款後並沒有立刻執行D2線程,而是被D3插隊了。

通過執行結果可以看出 公平鎖和非公平鎖的區別,公平鎖能保證等待線程優先執行,但是非公平鎖可能會被其它線程插隊。

四、ArrayBlockingQueue中關於ReentrantLock和Condition的應用

JDK源碼中關於可重入鎖的非常典型的應用是 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 是一個典型的生產者消費者模型,通過一個數組保存元素。為了保證添加和刪除元素的線程安全性,增加了可重入鎖和條件變量。

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

技術分享圖片

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

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

參考:

可重入鎖介紹:https://blog.csdn.net/yanyan19880509/article/details/52345422

IBM關於Lock介紹:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html

http://286.iteye.com/blog/2296249

java並發編程——通過ReentrantLock,Condition實現銀行存取款