1. 程式人生 > >java Condition條件變數的通俗易懂解釋

java Condition條件變數的通俗易懂解釋

最近在看pthread方面的書,看到條件變數一節的時候,回憶了下java中條件變數的使用方式。

java中條件變數都實現了java.util.concurrent.locks.Condition介面,條件變數的例項化是通過一個Lock物件上呼叫newCondition()方法來獲取的,這樣,條件就和一個鎖物件繫結起來了。因此,Java中的條件變數只能和鎖配合使用,來控制併發程式訪問競爭資源的安全。

條件變數的出現是為了更精細控制執行緒等待與喚醒,在Java5之前,執行緒的等待與喚醒依靠的是Object物件的wait()和notify()/notifyAll()方法,這樣的處理不夠精細。

通熟易懂的說,就是消費者/生產者的場景中,在原來的基礎上,增加了佇列滿時及時通知消費者,佇列空時及時通知生產者的優化,通常是兩個條件變數一起出現,一個控制值,但兩個條件變數可以毫無關係,終歸來說還是在Lock的範圍內。所以,從本質上來說,是對Object監視器的場景性優化,而不是全新機制的引入。

從現實應用角度來說,它們常被用於下列場景:

1、寫log。比如每1秒或者commit或者日誌大於1/3m時候都寫入。快取中大於1/3m時需要等寫入完成才能commit。

而在Java5中,一個鎖可以有多個條件,每個條件上可以有多個執行緒等待,通過呼叫await()方法,可以讓執行緒在該條件下等待。當呼叫signalAll()方法,又可以喚醒該條件下的等待的執行緒。有關Condition介面詳細說明可以具體參考JavaAPI文件。

如下:public class TestConditon

複製程式碼

public static void main(String[] args) {
        // 建立併發訪問的賬戶
        MyCount myCount = new MyCount("95599200901215522", 10000);
        // 建立一個執行緒池
        ExecutorService pool = Executors.newFixedThreadPool(3); #假設改成2會怎麼樣??
        Thread t1 = new SaveThread("張三", myCount, 1000);
        Thread t2 = new SaveThread("李四", myCount, 1000);
        Thread t3 = new DrawThread("王五", myCount, 12600);
        Thread t4 = new SaveThread("老張", myCount, 600);
        Thread t5 = new DrawThread("老牛", myCount, 1300);
        Thread t6 = new DrawThread("胖子", myCount, 800);
        Thread t7 = new SaveThread("測試", myCount, 2100);
        // 執行各個執行緒
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        pool.execute(t6);
        pool.execute(t7);
        // 關閉執行緒池
        pool.shutdown();
    }
}

/**
 * 存款執行緒類
 */
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);
    }
}

/**
 * 取款執行緒類
 */
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);
    }
}

/**
 * 普通銀行賬戶,不可透支
 */
class MyCount {
    private String oid; // 賬號
    private int cash; // 賬戶餘額
    private Lock lock = new ReentrantLock(); // 賬戶鎖
    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 {
            while (cash - x < 0) { 
                _draw.await(); // 阻塞取款操作, await之後就隱示自動釋放了lock,直到被喚醒自動獲取

                System.out.println(name + "阻塞中");
            }
            {
                cash -= x; // 取款
                System.out.println(name + "取款" + x + ",當前餘額為" + cash);
            }
            _save.signalAll(); // 喚醒所有存款操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 釋放鎖
        }
    }
}

複製程式碼

需要注意的是,在共用一個執行緒池的設計中,特別要注意餓死現象(就像上下高速如果公用車道的話,萬一進入的10車全部佔坑了,高速裡面又滿了的話,想出的都出不來,進的進不去,就出現餓死現象了),如果有大量的消費者使得生產者執行緒無法再執行的話,就會出現該問題,在上述例子中,將執行緒池數量從3改成2就可以多次測試中發現程式hang了。