Java高級上鎖機制:顯式鎖 ReentrantLock

Java 5.0 加入了新的上鎖工作:ReentrantLock馁菜,它和同步(Synchronized)方法的內(nèi)置鎖不同,這是一種顯式鎖。顯式鎖作為一種高級的上鎖工作引润, 是同步方法的一種補充和擴展,用來實現(xiàn)同步代碼塊無法完成的功能宗弯。

1 Lock和ReentrantLock

Lock作為顯式鎖脯燃,其提供了一種無條件的、可輪詢和定時的蒙保、可中斷的鎖操作辕棚,其獲得鎖和釋放鎖的操作都是顯示。

Lock是Java 5.0 中加入的接口邓厕,表示顯式鎖的功能逝嚎,其接口定義如下:

public interface Lock {
    void lock(); //獲取鎖
    void lockInterruptibly() throws InterruptedException; //可中斷的獲取鎖操作
    boolean tryLock(); //嘗試獲取鎖,不會被擁塞详恼,如果失敗立刻返回
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //在一定時間內(nèi)嘗試獲得鎖补君,如果超時則失敗
    void unlock(); // 釋放鎖
    Condition newCondition();
}

前文中,我們已經(jīng)討論過昧互,顯式鎖和同步代碼塊中的內(nèi)置鎖有著相同的互斥性和內(nèi)存可見性挽铁。ReentrantLockLock的一種實現(xiàn),提供對于線程的重入機制敞掘。和同步方法(Synchronized)相比叽掘,有著更強性能和靈活性。

雖然同步方法的內(nèi)置鎖已經(jīng)很強大和完備了玖雁,但是在功能上還有一定的局限性:不能實現(xiàn)非擁塞的鎖操作够掠。比如不能提供響應中斷的獲得鎖操作,不能提供支持超時的獲得鎖操作等等茄菊。因此疯潭,在某些情況下需要使用更為靈活的加鎖方式,也就是顯式鎖面殖。

在Java官方的注解中竖哩,給出了這樣的代碼示例:

 Lock l = new ReentrantLock();
 l.lock();
 try {
   // access the resource protected by this lock
 } finally {
   l.unlock();
 }

顯式鎖需要在手動調(diào)用lock方法來獲得鎖,并在使用后在finally代碼塊中調(diào)用unlock方法釋放鎖脊僚,以保證無論操作是否成功都能釋放掉鎖相叁。

顯式鎖支持非擁塞的鎖操作,具體的功能有:支持可輪詢和定時的辽幌、以及可中斷的鎖獲得操作增淹。

1.1 輪詢鎖和定時鎖

使用tryLock方法可以用于實現(xiàn)輪詢鎖定時鎖。和無條件的獲得鎖操作相比乌企,tryLock方法具有更完善的錯誤恢復機制虑润,可以避免死鎖的放生。相比之下加酵,同步方法發(fā)生死鎖拳喻,其恢復方法就只能重新啟動程序哭当。

避免死鎖的方式之一為打破“請求與保持條件”(死鎖的四個條件),比如在要獲得多個鎖才能工作的情況下冗澈,如果不能獲得全部的鎖钦勘,就會釋放掉已經(jīng)持有的鎖,一段時間之后再去重新嘗試獲得所有的鎖亚亲。也就是說要么獲得所有鎖彻采,要么一個鎖都不占有

下面的代碼中以轉賬為例捌归,演示了輪詢鎖的工作機制肛响。

public class DeadlockAvoidance {
    private static Random rnd = new Random();

    // 轉賬
    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) {
            // 嘗試獲得fromAcct的鎖
            if (fromAcct.lock.tryLock()) {
                try {
                    // 嘗試獲得toAcct的鎖
                    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鎖
                            toAcct.lock.unlock();
                        }
                    }
                } finally { //釋放fromAcct鎖
                    fromAcct.lock.unlock();
                }
            }
            // 獲得鎖失敗
            // 判斷是否超時 如果超時則立刻失敗
            if (System.nanoTime() < stopTime)
                return false;

            // 如果沒有超時陨溅,隨機睡眠一段時間
            NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
        }
    }


    class Account {
        //顯示鎖
        public Lock lock;

        void debit(DollarAmount d) {
        }

        void credit(DollarAmount d) {
        }

        DollarAmount getBalance() {
            return null;
        }
    }

    class InsufficientFundsException extends Exception {
    }
}

只有同時獲得轉出賬戶和轉入賬戶的鎖后终惑,才會進行轉賬绍在。如果不能同時獲得兩個鎖门扇,就釋放掉已經(jīng)獲得的鎖,并隨機隨眠一段時間偿渡,再去嘗試獲得全部的鎖臼寄,循環(huán)這個過程直到超時。

除了輪詢申請獲得鎖之外溜宽,也可以使用帶有時間限制的定時鎖操作吉拳,即獲得鎖的操作具有時間限制,超過一定時間后仍沒有獲得鎖就會返回失敗适揉。示例如下:

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

    public boolean trySendOnSharedLine(String message,
                                       long timeout, TimeUnit unit)
            throws InterruptedException {
        // 設定超時時間
        long nanosToLock = unit.toNanos(timeout)
                - estimatedNanosToSend(message);
        // 在規(guī)定時間內(nèi)等待鎖 否者就會返回false
        if (!lock.tryLock(nanosToLock, NANOSECONDS))
            return false;
        try {
            return sendOnSharedLine(message);
        } finally {
            lock.unlock();
        }
    }

    private boolean sendOnSharedLine(String message) {
        /* send something */
        return true;
    }

    long estimatedNanosToSend(String message) {
        return message.length();
    }
}

1.2 中斷鎖

如果要將顯式鎖應用到可以取消的任務重留攒,就需要讓獲得鎖的操作是支持中斷。 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;
    }

}

1.3 非塊結構的加鎖

在內(nèi)置鎖中剪侮,鎖的獲得和鎖的釋放都是在同一塊代碼的拭宁,這樣簡潔清楚還便于使用,不用考慮如何退出代碼塊瓣俯。但是加鎖的位置不一定只有代碼塊杰标,比如之前談過的分段鎖ConcurrentHashMap中利用了分段鎖對散列表中的元素分段上鎖彩匕,實現(xiàn)了并發(fā)訪問容器元素的功能腔剂。如果是這種非塊結構的加鎖,就不能應用內(nèi)置鎖驼仪,而是需要使用顯式鎖控制桶蝎。同樣驻仅,鏈表類的容器可以應用分段鎖,來支持并發(fā)訪問不同鏈表元素登渣。

2 性能因素考慮

前文中曾經(jīng)提過噪服,ConcurrentHashMap和同步的HashMap相比,其性能優(yōu)勢在于利用了分段鎖對散列表中的元素分段上鎖胜茧,故而支持并發(fā)訪問容器中不同的元素粘优。同理,和內(nèi)置鎖相比呻顽,顯式鎖都優(yōu)勢在于更好的性性雹顺。鎖的實現(xiàn)方式越好,就越可以避免不必要的系統(tǒng)調(diào)用和上下文切換廊遍,以提高效率嬉愧。

線程間的切換,涉及線程掛起和恢復等一系列操作喉前,這樣的線程上下文的切換很是消耗性能没酣,所以要避免不必要的線程切換。

Java 6中對內(nèi)置鎖的進行了優(yōu)化卵迂,現(xiàn)在內(nèi)置鎖和顯式鎖相比性能已經(jīng)很接近裕便,只略低一些。

3. 公平鎖

ReentrantLock的構造函數(shù)中提供兩種鎖的類型:

  • 公平鎖:線程將按照它們請求鎖的順序來獲得鎖见咒;
  • 非公平鎖:允許插隊偿衰,如果一個線程請求非公平鎖的那個時刻,鎖的狀態(tài)正好為可用改览,則該線程將跳過所有等待中的線程獲得該鎖下翎。

非公平鎖在線程間競爭鎖資源激烈的情況下,性能更高宝当,這是由于:在恢復一個被掛起線程與該線程真正開始運行之間视事,存在著一個很嚴重的延遲,這是由于線程間上下文切換帶來的今妄。正是這個延遲郑口,造成了公平鎖在使用中出現(xiàn)CPU空閑。非公平鎖正是將這個延遲帶來的時間差利用起來盾鳞,優(yōu)先讓正在運行的線程獲得鎖犬性,避免線程的上下文切換。

如果每個線程獲得鎖的時間都很長腾仅,或者請求鎖的競爭很稀疏或不頻繁乒裆,則公平鎖更為適合。

內(nèi)置鎖和顯式鎖都是默認使用非公平鎖推励,但是顯式鎖可以設置公平鎖鹤耍,內(nèi)置鎖無法做到肉迫。

4. 同步方法和顯式鎖的選擇

顯式鎖雖然更為靈活,提供更為豐富的功能稿黄,且性能更好喊衫,但是還是推薦先使用同步(Synchronized)方法,這是因為同步方法的內(nèi)置鎖杆怕,使用起來更為方便族购,簡潔緊湊 ,還便于理解陵珍,也更為開發(fā)人員所熟悉寝杖。

建議只有在一些內(nèi)置鎖無法滿足的情況下,再將顯式鎖ReentrantLock作為高級工具使用互纯,比如要使用輪詢鎖瑟幕、定時鎖、可中斷鎖或者是公平鎖留潦。除此之外只盹,還應該優(yōu)先使用synchronized方法。

5. 讀-寫鎖

無論是顯式鎖還是內(nèi)置鎖愤兵,都是互斥鎖鹿霸,也就是同一時刻只能有一個線程得到鎖排吴「讶椋互斥鎖是保守的加鎖策略,可以避免“寫-寫”沖突钻哩、“寫-讀”沖突”和"讀-讀"沖突屹堰。但是有時候不需要這么嚴格 ,同時多個任務讀取數(shù)據(jù)是被允許街氢,這有助于提升效率扯键,不需要避免“讀-讀”操作。為此珊肃,Java 5.0 中出現(xiàn)了讀-寫鎖ReadWriteLock荣刑。

ReadWriteLock可以提供兩種鎖:

  • 讀鎖readLock:允許多個線程同時執(zhí)行讀操作,但是同時只能有一個線程執(zhí)行寫操作伦乔;
  • 寫鎖writeLock:正常的互斥鎖厉亏,同一時刻只能有一個線程執(zhí)行讀寫操作。

ReentrantReadWriteLock是讀寫鎖支持重入的實現(xiàn)烈和,下面的例子中利用讀寫鎖實現(xiàn)了支持并發(fā)讀取元素的多線程安全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 get(Object key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    .....
}

不過需要注意的是爱只,雖然讀寫鎖的出現(xiàn)是為了提高效率,但只適用于對多線程頻繁并發(fā)執(zhí)行讀操作的情況招刹。如果是在正常的情況下使用讀寫鎖恬试,反而會降低效率窝趣,因為ReadWriteLock需要額外的開銷維護分別維護讀鎖和寫鎖,得不償失训柴。

擴展閱讀:

  1. 多線程安全性:每個人都在談哑舒,但是不是每個人都談地清
  2. 對象共享:Java并發(fā)環(huán)境中的煩心事
  3. 從Java內(nèi)存模型角度理解安全初始化
  4. 從任務到線程:Java結構化并發(fā)應用程序
  5. 關閉線程的正確方法:“優(yōu)雅”的中斷
  6. 駕馭Java線程池:定制與擴展
  7. 探秘Java并發(fā)模塊:容器與工具類
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市幻馁,隨后出現(xiàn)的幾起案子散址,更是在濱河造成了極大的恐慌,老刑警劉巖宣赔,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件预麸,死亡現(xiàn)場離奇詭異,居然都是意外死亡儒将,警方通過查閱死者的電腦和手機吏祸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钩蚊,“玉大人贡翘,你說我怎么就攤上這事∨槁撸” “怎么了鸣驱?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蝠咆。 經(jīng)常有香客問我踊东,道長,這世上最難降的妖魔是什么刚操? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任闸翅,我火速辦了婚禮,結果婚禮上菊霜,老公的妹妹穿的比我還像新娘坚冀。我一直安慰自己,他們只是感情好鉴逞,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布记某。 她就那樣靜靜地躺著,像睡著了一般构捡。 火紅的嫁衣襯著肌膚如雪液南。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天叭喜,我揣著相機與錄音贺拣,去河邊找鬼。 笑死,一個胖子當著我的面吹牛譬涡,可吹牛的內(nèi)容都是我干的闪幽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼涡匀,長吁一口氣:“原來是場噩夢啊……” “哼盯腌!你這毒婦竟也來了?” 一聲冷哼從身側響起陨瘩,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤腕够,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后舌劳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帚湘,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年甚淡,在試婚紗的時候發(fā)現(xiàn)自己被綠了大诸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡贯卦,死狀恐怖资柔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撵割,我是刑警寧澤贿堰,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站啡彬,受9級特大地震影響羹与,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜外遇,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一注簿、第九天 我趴在偏房一處隱蔽的房頂上張望契吉。 院中可真熱鬧跳仿,春花似錦、人聲如沸捐晶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惑灵。三九已至山上,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間英支,已是汗流浹背佩憾。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妄帘。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓楞黄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抡驼。 傳聞我的和親對象是個殘疾皇子鬼廓,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容