同步——鎖對象和條件對象

同步

在大多數實際的多線程應用中,兩個或兩個以上的線程需要共享對同一數據的存取。如果每一個線程都調用修改了該數據,可能會產生錯誤,這種情況叫做競爭條件。爲了避免多線程對共享數據的訛誤,就需要學習如何同步存取。

鎖對象

有兩種機制防止代碼塊受併發訪問的干擾:

1)synchronized關鍵字

2)ReentrantLock類

用ReentrantLock保護代碼塊的基本機構如下:

        Lock lock=new ReentrantLock();
        lock.lock();
        try {
            System.out.println("進入lock鎖");
            account--;
        } finally {
            lock.unlock();
        }

這一結構確保任何時刻只有一個線程進入臨界區。一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句。當其他線程調用lock時,它們被阻塞,直到第一個線程釋放鎖對象。

注意:把解鎖操作放在finally子句之內是至關重要的。如果在臨界區的代碼拋出異常,鎖必須被釋放。否則,其他線程將永遠阻塞。

每一個實例對象都有自己的ReentrantLock對象。假設現在有Bank類,如果兩個線程試圖訪問同一個Bank對象,那麼鎖以串行方式提供服務。但是,如果連個線程訪問不同的Bank對象,每一個線程得到不同的鎖對象,兩個線程都不會發生阻塞。本該如此,因爲線程在操縱不同的Bank實例的時候,線程之間不會相互影響

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

public class LockDemo {

    private static Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        new Thread(()-> {lockTest();}).start();
        new Thread(){
            @Override
            public void run() {
                lockTest();
            }
        }.start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("進入lock鎖1");
            lock.lock();
            try {
                System.out.println("進入lock鎖2");
            }finally {
                lock.unlock();
            }
            System.out.println("出鎖2");
        } finally {
            lock.unlock();
        }
        System.out.println("出鎖1");
        System.out.println("===============");
    }
}

對於lock加鎖和釋放鎖的問題,首先開啓了兩個線程進行打印(注意需要使用一個lock對象)。下圖是正常打印的結果:

public class LockDemo {
    private static Lock lock=new ReentrantLock();

    public static void main(String[] args) {
        new Thread(()-> {lockTest();}).start();
        new Thread(){
            @Override
            public void run() {
                lockTest();
            }
        }.start();
    }
    private static void lockTest(){
        System.out.println(Thread.currentThread().getName());
        lock.lock();
        try {
            System.out.println("進入lock鎖1");
            lock.lock();
            try {
                System.out.println("進入lock鎖2");
            }finally {
                // 1
                lock.unlock();
            }
            System.out.println("出鎖2");
        } finally {
              // 2
            //lock.unlock();
        }
        System.out.println("出鎖1");
        System.out.println("===============");
    }
}

註釋掉1或者2處的lock.unlock(),此時釋放鎖的次數不等於加鎖的次數。

結果顯示,已經發生了死鎖。

ReentrantLock():構建一個可以被用來保護臨界區的可重入鎖。

ReentrantLock(boolean fair):構建一個帶有公平策略的鎖。一個公平鎖偏愛等待時間最長的線程。但是,這一公平的保證將大大降低性能。所以,默認情況下,鎖沒有被強制爲公平的。

條件對象

通常,線程進入臨界區,卻發現在某一個條件滿足之後它才能執行。要使用一個條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程。

先看一個簡單的例子:
 

public class ConditionDemo {
    private static Lock lock=new ReentrantLock();
    private static int i=10;
    private static Condition condition=lock.newCondition();

    public static void main(String[] args){
       new Thread(()->conditionTest()).start();
       new Thread(()-> {lockTest();}).start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("進入lock鎖1");
            i++;
        } finally {
            System.out.println("釋放鎖1");
            lock.unlock();
        }
    }

    //測試條件對象
    private static void conditionTest(){
        lock.lock();
        try{
            System.out.println("獲得鎖");
            while(i<=10){
                System.out.println("進入條件對象中");
            }
            System.out.println("第二次獲得鎖");
        }finally {
            lock.unlock();
        }
    }
}

上面的程序跑起來的結果就是無限制的循環打印——“進入條件對象中”。如果正常執行第二個線程lockTest的i++,會得到正確的輸出。但是,while的判斷條件會讓這一線程一直持有鎖,並不給其他線程操作的機會。此時我們可以主動讓當前線程阻塞,並放棄鎖,這就是爲什麼使用條件對象的原因。

public class ConditionDemo {
    private static Lock lock=new ReentrantLock();
    private static int i=10;
    private static Condition condition=lock.newCondition();

    public static void main(String[] args){
       new Thread(()->conditionTest()).start();
       new Thread(()-> {lockTest();}).start();
    }
    private static void lockTest(){
        lock.lock();
        try {
            System.out.println("進入lock鎖1");
            i++;
            //喚醒等待集中的所有線程
            condition.signalAll();
        } finally {
            System.out.println("釋放鎖1");
            lock.unlock();
        }
    }
    //測試條件對象
    private static void conditionTest(){
        lock.lock();
        try{
            System.out.println("獲得鎖");
            while(i<=10){
                System.out.println("進入條件對象中");
                //一旦一個線程調用await方法,它進入該條件的等待集,當鎖可用時,該線程不能馬上解除                
                //阻塞.相反,它處於阻塞狀態,知道另一個線程調用同一條件上的signalAll方法時爲止.
                condition.await();
                System.out.println("被另一線程喚醒");
            }
            System.out.println("第二次獲得鎖");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

正確是執行結果:

signalAll的調用重新激活因爲這一條件而等待的所有線程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖重新進入該對象。一旦鎖成爲可用的,它們中的某個將從await調用返回,獲得該鎖並從被阻塞的地方繼續執行。

此時,線程應該再次測試該條件。由於無法確保該條件被滿足——signalAll方法僅僅是通知正在等待的線程:此時有可能已經滿足條件,值得再去檢測該條件。

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

比如,去掉代碼中的 condition.signalAll(); 最終運行結果如下:

即使鎖已經被釋放,但是沒有其他線程的喚醒,該阻塞線程也無法自喚醒重新獲取鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章