天天看點

Thread專題(12) - 顯式鎖

此文被筆者收錄在系列文章 ​​​架構師必備(系列)​​ 中

在java5.0之前,對象共享通路的機制隻有synchronized和volatile。内部鎖不能中斷那些正在等待擷取鎖的線程,并且在請求鎖失敗的情況下,必須無限等待。在5.0之後提供了ReentrantLock,ReentrantLock并不是為了替代内部鎖,提供可選擇的進階特性,比如活躍度和性能。

一、Lock和ReentrantLock

Lock接口,定義了一些抽象的鎖操作,與内部加鎖機制不同,Lock提供了無條件的、可輪詢的、定時的、可中斷的鎖操擷取操作,所有鎖的方法也是顯式的。Lock的實作必須提供具有與内部加鎖相同的記憶體可見性的語義。

Lock接口的規範形式,鎖必須在finally塊中釋放。原因是如果鎖守護的代碼在try塊之外抛出了異常,它将永遠都不會被釋放,如果對象能夠被置于不一緻的狀态,可能需要額外的try-catch或try-finally塊。而且顯式鎖并不會主動釋放,如果忘記寫fianlly塊,則程式很難追蹤的到。

Lock lock = new ReentrantLock();
lock.lock();
try{
    
}catch (Exception e){
    
}
finally{
    lock.unlock();
}      

可輪詢和可定時的鎖請求

可定時和可輪詢的鎖擷取模式是由tryLock方式實作,與無條件的鎖擷取相比,它具有更完善的錯誤恢複機制。在内部鎖中,死鎖-唯一的恢複方法是重新啟動程式,唯一的預防方法是在建構程式時不要出錯,可定時與可輪詢的鎖提供了另一個規避死鎖發生的方法。

如果不能獲得所有需要的鎖,可以用定時與可輪詢的擷取方式重新拿到控制權,它會釋放你已經獲得的這些鎖,然後再重新嘗試(至少可以記錄這個失敗)。

class Account {
        public Lock lock;
        void debit(DollarAmount d) {
        }
        void credit(DollarAmount d) {
        }
        DollarAmount getBalance() {
            return null;
        }
    }      
public boolean transferMoney(Account fromAcct,
                                 Account toAcct,
                                 DollarAmount amount,
                                 long timeout,
                                 TimeUnit unit)
            throws InsufficientFundsException, InterruptedException {
        long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
        long randMod = getRandomDelayModulusNanos(timeout, unit);
        //加入這個随機數是為了減少活鎖的可能性
        long stopTime = System.nanoTime() + unit.toNanos(timeout);

        while (true) {
            if (fromAcct.lock.tryLock()) {
                try {
                    if (toAcct.lock.tryLock()) {
                        try {
                       if (fromAcct.getBalance().compareTo(amount) < 0)
                                throw new InsufficientFundsException();
                            else {
                                fromAcct.debit(amount);
                                toAcct.credit(amount);
                                return true;
                            }
                        } finally {
                            toAcct.lock.unlock();
                        }
                    }
                } finally {
                    fromAcct.lock.unlock();
                }
            }
            if (System.nanoTime() < stopTime)
                return false;
            NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
        }
    }      

對于實作有時間限制的活動,定時鎖能夠在時間預算内設定相應的逾時,如果活動在期待的時間内沒能獲得結果,這個機制使程式能提前傳回。

串行化通路資源,一種方法是單線程、另一個方法是使用獨占鎖來守護對資源的通路。定時的tryLock與獨占鎖互相配合,可以很好的解決有時間限制的活動的這樣的程式。

//具有預定時間的鎖
private Lock lock = new ReentrantLock();

    public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit)
            throws InterruptedException {
        long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
        if (!lock.tryLock(nanosToLock, NANOSECONDS))
            return false;
        try {
            return sendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }      

可中斷的鎖擷取

當你正在響應中斷的時候,lockInterruptibly方法能夠使你重新獲得鎖。

public class InterruptibleLocking {
    private Lock lock = new ReentrantLock();
    public boolean sendOnSharedLine(String message)
            throws InterruptedException {
        lock.lockInterruptibly();
        try {
            return cancellableSendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }
    private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
        /* send something */
        return true;
    }
}      

非塊結構的鎖

在内部鎖中,擷取和釋放這樣成對的行為是塊結構的---總是在其獲得的相同的基本程式塊中釋放鎖,而不考慮控制權是如何退出阻塞的。自動釋放鎖簡化了程式的分析,并避免了潛在的代碼造成的麻煩,但是有時需要更靈活的加鎖規則。

分離鎖時不同的哈希鍊在哈希容器中使用不同的鎖,在連結清單中,我們可以通過為每個連結清單節點應用相似的原則來減小鎖的粒度,進而允許不同的線程獨立地操作連結清單的不同部分。給定節點的鎖守護連結的指針,資料就存儲在該節點中,是以如果要周遊或修改連結清單,我們必須得到這個鎖,并持有它直到我們獲得了下一個節點的鎖;隻有在這之後我們才能釋放前一個節點的鎖。這項技術的例子被稱為連鎖式加鎖或鎖聯接。

二、公平性

ReentrantLock構造函數提供了兩種公平性的選擇,建立非公平鎖(預設實作)或者公平鎖。線程按順序請求獲得公平鎖,然而一個非公平鎖允許“闖入”,當請求這樣的鎖時,如果鎖的狀态變為可用,線程的請求可以在等待線程的隊列中向前跳躍,獲得該鎖(Semaphore同樣提供了公平和非公平的擷取順序)。即使對于公平鎖而言,可輪詢(while循環)的tryLock總會闖入。

在激烈競争的情況下,闖入鎖比公平鎖性能更好,原因之一:挂起的線程重新開始,與它真正開始運作,兩者之間會産生嚴重的延遲;當線程持有鎖的時間相對較長,或者請求鎖的平均時間間隔比較長,那麼使用公平鎖是比較好的。

正如預設的ReentrantLock一樣,内部鎖沒有提供确定的公平性保證,但是大多數鎖實作統計上的公平性保證,在大多數條件下已經足夠好了。java語言規範并沒有要求JVM公平地實作内部鎖,JVM也的确沒有這樣做,ReentrantLock并沒有減少鎖的公平性--它隻不過使一些存在的部分更顯性化了。

三、在synchronized和ReentrantLock之間決擇

他們在語義、定時鎖的等待、可中斷鎖的等待、公平性、以及實作非塊結構的鎖基本都是相同的。ReentrantLock的性能看起來勝過内部鎖,雖然這樣,但是一般來說不應該混合使用這兩種方式。

隻有在synchronized不能滿足需求的時間,才需要使用ReentrantLock。這些需求包括:可定時的、可輪詢的與可中斷的鎖擷取操作、公平隊列、非塊結構的鎖,否則請使用synchronized。

内部鎖與ReentrantLock相比,還具有另一個優點:線程轉儲能夠顯示哪些個調用架構獲得了哪些鎖,并能夠識别發生了死鎖的那些線程,JVM并不知道哪個線程持有ReentrantLock,是以在調試使用ReentrantLock的線程間存在的問題,可以調用管理和調試接口,但隻局限于JAVA 6。

四、讀-寫鎖

ReentrantLock是一個标準的互斥鎖,一次最多隻有一個線程能夠持有相同的ReentrantLock。但是互斥通常作為保護資料一緻性的很強的加鎖方式,是以過分地限制了并發性。互斥是保守的加鎖政策,避免了“寫/寫”、“寫/讀”、“讀/讀”的重疊,隻要一個資源能夠被多個讀者通路,或者被一個寫者通路,兩者不能同時進行。讀取ReadWriteLock鎖守護的資料,必須首先獲得讀取的鎖,當需要修改ReadWriteLock守護的資料時,必須首先獲得寫入的鎖。

interface ReadWriteLock{
  Lock read();
  Lock write();
}      

ReadWriteLock這個接口是一個簡單的讀-寫鎖實作,ReadWriteLock允許多種實作,這些實作可以在性能、排程保證、擷取優先級、公平性、加鎖主義等方面不盡相同。讀-與鎖的設計是用來進行性能改進的,使得特定情況下能夠有更好的并發性。讀取和寫入鎖之間的互動可以有很多種實作,比如:

1、釋放優先:當寫者釋放寫入鎖,并且讀者和寫者都排在隊列中,應該選擇哪個--讀者,寫者還是其它的。

2、讀者闖入:如果鎖由讀者獲得,但是有寫者正在等待,那麼新到達的寫者應該被授予讀取的權力、還是等待。允許讀者闖入到寫者之前提高了并發性,但是卻帶來了寫饑餓的風險。

  • 重進入:讀和寫允許重入嗎?
  • 降級:如果線程持有寫入的鎖,它能夠在不釋放該鎖的情況下獲得讀取鎖麼?這可能會造成寫者“降級”為一個讀取鎖。讀的級别比寫要高。
  • 更新:同降級。 這可能會造成死鎖。

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();
        }
    }

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

小結