1. 程式人生 > >併發程式設計學習筆記之顯示鎖(十一)

併發程式設計學習筆記之顯示鎖(十一)

ReentrantLock(重進入鎖)並不是作為內部鎖(synchronized)機制的替代,而是當內部鎖被證明受到侷限時,提供可選擇的高階特性.

1. Lock 和 ReentrantLock

Lock介面:

public interface Lock {

 
    void lock();

    
    void lockInterruptibly() throws InterruptedException;

   
    boolean tryLock();

    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    
    void unlock();

   
    Condition newCondition();
}

與內部加鎖機制不同,Lock提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯示的.

Lock的實現必須提供具有與內部加鎖相同的記憶體可見性的語義.但是加鎖的語義、排程演算法、順序保證,效能特性這些可以不同.

ReentrantLock實現了Lock介面,提供了與synchronized相同的互斥和記憶體可見性的保證.

獲得ReentrantLock的鎖與進入synchronized塊有著相同的記憶體語義,釋放ReentrantLock鎖與退出synchronized塊有相同的記憶體語義.

ReentrantLock提供了與synchronized一樣的可重入加鎖的語義.ReentrantLock支援Lock介面定義的所有獲取鎖的模式.

一句話synchronized能做的,ReentrantLock都能做,但是ReentrantLock為處理不可用的鎖提供了更多靈活性(好吧,ReentrantLock寫起來比較麻煩)

為什麼要使用顯示鎖

內部鎖在大部分情況下都能很好地工作,但是有一些功能上的侷限--不能中斷那些正在等待獲取鎖的執行緒,並且在請求鎖失敗的情況下,必須無限等待.

內部鎖必須在獲取他們的程式碼塊中被釋放:這很好地簡化了程式碼,與異常處理機制能夠良好的互動,但是在某些情況下,一個更靈活的加鎖機制提供了更好的活躍度和效能.

public class LockTest {
        Lock lock  = new ReentrantLock();

        public void testLock(){
            lock.Lock();
            try {
            // 需要加鎖的程式碼..

            }finally {
                lock.unlock();
            }
        }
}

這個模式在某種程度上比使用內部鎖更加複雜:鎖必須在finally塊中釋放.

另一方面,如果鎖守護的程式碼在try塊之外丟擲了異常,它將永遠都不會被釋放了;

如果物件能夠被置於不一致的狀態,可能需要額外的try-catch,或try-finally塊.

顯示的lock的缺點

使用lock之後必須unlock釋放鎖,這也是ReentrantLock不能完全替代synchronized的原因.

它更加危險,因為當程式的控制權離開了守護的塊時,不會自動清除鎖.

1.1 可輪詢和可定時的鎖請求

可定時的與可輪詢的鎖獲取模式,是由tryLock方法實現,與無條件的鎖獲取相比,它具有更完善的錯誤恢復機制.

使用內部鎖,發生死鎖時唯一的恢復方法是重啟程式,唯一的預防方法是在構建程式時不要出錯,所以不可能允許不一致的鎖順序.

可定時的與可輪詢的鎖提供了另一個選擇,可以規避死鎖的發生.

使用方式:

public class LockSample {
    //建立一個鎖的例項
    Lock lock = new ReentrantLock();

    public void methodA(){
        lock.lock();
        try {
            System.out.println("執行了方法A");
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        lock.lock();
        try {
            System.out.println("執行了方法B");
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


    public static void main(String [] args){

        LockSample lockSample = new LockSample();
        lockSample.methodA();
        //methodB()方法必須在鎖可用的時候才會執行
        lockSample.methodB();
    }  
}

使用tryLock能解決第九篇部落格死鎖,提到過的動態的順序死鎖問題.

public class LockTest {

    public static void main(String [] args){
                LockTest lockTest = new LockTest();
        Account fromAccount = new Account();
        Account toAccount = new Account();
        Account account = new Account();
        //開啟一個新執行緒,獲取兩個使用者的鎖,這個方法是假設,物件的鎖已經被獲得用的.
        new Thread(){
            @Override
            public void run(){
                //這兩個方法的內部實現就是Thread.sleep()將程式碼阻塞住.
                fromAccount.credit(account);
                toAccount.dedit(account);
            }
        }.start();


        lockTest.transferMoney(fromAccount,toAccount,account);
    }

        public void transferMoney(Account fromAccount,Account toAccount,Account account){


            while(true){
                // lock.tryLock()返回一個布林值,告訴你當前的鎖是否可用,如果可用往下走
            if(fromAccount.lock.tryLock()){
                try {
                if (toAccount.lock.tryLock()){
                    try {
                        //走到這裡,證明兩個鎖都可用,可以進行轉賬操作.
                        fromAccount.credit(account);
                        toAccount.dedit(account);
                    }finally {
                        toAccount.lock.unlock();
                    }
                }
                }finally {
                    fromAccount.lock.unlock();
                }
            }
        }

        }

}

Account的內部實現:

public class Account {
    public Lock lock = new ReentrantLock();

    public void credit(Account account) {
        lock.lock();
        try {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }

    }

    public void dedit(Account account) {
        lock.lock();
        try {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }

    }

定時鎖可以在時間預算內設定相應的超時,如果活動子啊期待的時間內沒能獲得結果,這個機制是程式能夠提前返回.

而使用內部鎖一旦開始請求,鎖就不能停止了,所以內部鎖為實現具有時限的活動帶來了風險.

.tryLock方法還有一個過載版本,可以設定等待的時間:

lock.tryLock(4, TimeUnit.SECONDS)

1.2 可中斷的鎖獲取操作

lock.lockInterruptibly上的鎖,是可以響應中斷的:

public class LockSample {
    //建立一個鎖的例項
    Lock lock = new ReentrantLock();

    public void testInterruptibly(){
        try {
            lock.lockInterruptibly();

            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }


    public void test(){
        System.out.println("lock.tryLock() = " + lock.tryLock());
        try {
            System.out.println("lock.tryLock(4,TimeUnit.SECONDS) = " + lock.tryLock(4, TimeUnit.SECONDS));;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodA(){
        lock.lock();
        try {
            System.out.println("執行了方法A");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void methodB(){
        lock.lock();
        try {
            System.out.println("執行了方法B");
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


    public static void main(String [] args){
        Long startTime = System.nanoTime();
        LockSample lockSample = new LockSample();
        Thread thread = Thread.currentThread();
        new Thread(){
            @Override
            public void run(){
                try {
                    //休眠兩秒,執行中斷
                    Thread.sleep(2000);
                    thread.interrupt();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
        //這裡本來是休眠5秒的,因為上面直接中斷了,可以看下面的endtime是兩秒,證明了可以被中斷
        lockSample.testInterruptibly();
        Long endTime = startTime - System.nanoTime();
        System.out.println("endTime = " + endTime);
    }
}

2. 對效能的考量

ReentrantLock提供的競爭上的效能要遠遠優於內部鎖.

對於同步原語而言,競態時的效能是可伸縮性的關鍵:若果有越多的資源花費在鎖的管理和排程上,那程式執行的時間就越少.

在Java5.0中,ReentrantLock相比於synchronized能給吞吐量帶來相當不錯的提升,但是在Java6中,這兩者非常接近.

也就是說之前選擇顯示鎖,還有效能方面的考量,但是現在顯示鎖和synchronized已經差不多了.

3. 公平性

ReentrantLock建構函式提供了兩種公平性的選擇:

  • 建立非公平鎖(預設)
  • 公平鎖

公平鎖:如果鎖已經被其他執行緒佔有,新的請求執行緒會加入到等待佇列,或者已經有一些執行緒在等待鎖了;

非公平鎖: 非公平鎖允許闖入,當請求這樣的鎖時,如果鎖的狀態變為可用,執行緒的請求可以在等待執行緒的佇列中向前跳躍,獲得該鎖.(Semaphore同樣提供了公平和非公平的獲取順序).在非公平的鎖中,執行緒只有當鎖正在被佔用時才會等待.

為什麼要使用不公平鎖

當發生加鎖的時候,公平會因為掛起和重新開始執行緒的代價帶來巨大的效能開銷.

在多數情況下,非公平鎖的優勢超過了公平的排隊.

在競爭激烈的情況下,闖入鎖比公平鎖效能好的原因之一是:掛起的執行緒重新開始,與它真正開始執行,兩者之間會產生嚴重的延遲.

比較公平鎖和非公平鎖,使用的例子:

假設執行緒A持有一個鎖,執行緒B請求該鎖.因為此時鎖正在使用中,執行緒B被掛起,當A釋放鎖後,B重新開始.與此同時,如果C請求鎖,那麼C得到了很好的機會獲得這個鎖,使用它,並且甚至可能在B被喚醒前就已經釋放該鎖了.

在這樣的情況下,各方面都獲得了成功:B並沒有比其他任何執行緒晚得到鎖,C更早的得到了鎖,吞吐量得到了改進.

如果持有鎖的時間相對較長,或者請求鎖的平均時間間隔較長,那麼使用公平鎖是比較好的.

4. 在synchronized和ReentrantLock之間進行選擇

在內部鎖不能夠滿足使用時,ReentrantLock才被作為更高階的工具,當你需要以下高階特性時,才應該使用:

可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,或者非塊結構的鎖,否則,請使用synchronized.

5. 讀-寫鎖

讀-寫鎖:一個資源能夠被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行.

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock暴露了兩個Lock物件,一個用來讀,另一個用來寫.讀取ReadWriteLock鎖守護的資料,你必須首先獲得讀取的鎖,當需要修改ReadWriteLock守護的資料時,你必須首先獲得寫入的鎖.

讀-寫鎖實現的加鎖策略允許多個同時存在的讀者,但是隻允許一個寫者.

讀-寫鎖的設計是用來進行效能改進的,使得特定情況下能夠有更好的併發性.

多處理器系統中,頻繁的訪問主要為讀取資料結構的時候,讀-寫鎖能夠改進效能;

在其他情況下執行的情況比獨佔的鎖要稍差一些,這歸因於它更大的複雜性.

ReentrantReadWriteLock也能被構造為非公平(預設)或公平的.

公平: 在公平的鎖中,選擇權交給等待時間最長的執行緒;如果鎖由讀者獲得,而一個執行緒請求寫入鎖,那麼不在允許讀者獲得讀取鎖,直到寫者被受理,並且已經釋放了寫入鎖.

非公平: 執行緒允許訪問的順序是不定的.由寫者降級為讀者是允許的;從讀者升級為寫者是不允許的(嘗試這樣的行為會導致死鎖).

使用讀寫鎖的情況

當鎖被持有的時間相對較長,並且大部分操作都不會改變鎖守護的資源,那麼讀-寫鎖能夠改進併發性.

使用讀-寫鎖包裝map:

public class ReadWriteMap<K,V> {

    private final Map<K,V> map;

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private final Lock r = lock.readLock();

    private final Lock w = lock.writeLock();

    public ReadWriteMap(Map<K, V> map) {
        this.map = map;
    }

    public V put(K key,V value){
        w.lock();
        try {
            return map.put(key,value);
        }finally {
            w.unlock();
        }
    }
    //remove(),putAll(),clear()使用w.lock


    public V get(Object key){
        r.lock();
        try {
            return map.get(key);
        }finally {
            r.unlock();
        }
    }

    //其他的只讀map使用r.lock

}

總結

顯示的Lock與內部鎖相比提供了一些擴充套件的特性,包括處理不可用的鎖時更好的靈活性,以及對佇列行為更好的控制,但是ReentrantLock不能完全替代synchronized;只有當你需要synchronized沒能提供的特性時才應該使用.

讀-寫鎖允許多個讀者併發訪問被守護的物件,當訪問多為讀取資料結構的時候,它具有改進可伸縮性的能力.