Java多線程:線程間通信之Lock

Java 5 之后桨昙,Java在內(nèi)置關(guān)鍵字sychronized的基礎(chǔ)上又增加了一個(gè)新的處理鎖的方式,Lock類(lèi)工秩。

由于在Java線程間通信:volatile與sychronized中廉白,我們已經(jīng)詳細(xì)的了解了synchronized,所以我們現(xiàn)在主要介紹一下Lock乖菱,以及將Lock與synchronized進(jìn)行一下對(duì)比坡锡。

1. synchronized的缺陷

synchronized修飾的代碼只有獲取鎖的線程才能夠執(zhí)行蓬网,其他線程只能等待該線程釋放鎖。一個(gè)線程釋放鎖的情況有以下方式:

  • 獲取鎖的線程完成了synchronized修飾的代碼塊的執(zhí)行鹉勒。
  • 線程執(zhí)行時(shí)發(fā)生異常帆锋,JVM自動(dòng)釋放鎖。

我們?cè)?a target="_blank" rel="nofollow">Java多線程的生命周期禽额,實(shí)現(xiàn)與調(diào)度中談過(guò)锯厢,鎖會(huì)因?yàn)榈却齀/O,sleep()方法等原因被阻塞而不釋放鎖脯倒,此時(shí)如果線程還處于用synchronized修飾的代碼區(qū)域里实辑,那么其他線程只能等待,這樣就影響了效率藻丢。因此Java提供了Lock來(lái)實(shí)現(xiàn)另一個(gè)機(jī)制剪撬,即不讓線程無(wú)限期的等待下去。

思考一個(gè)情景悠反,當(dāng)多線程讀寫(xiě)文件時(shí)残黑,讀操作和寫(xiě)操作會(huì)發(fā)生沖突,寫(xiě)操作和寫(xiě)操作會(huì)發(fā)生沖突斋否,但讀操作和讀操作不會(huì)有沖突梨水。如果使用synchronized來(lái)修飾的話,就很可能造成多個(gè)讀操作無(wú)法同時(shí)進(jìn)行的可能(如果只用synchronized修飾寫(xiě)方法如叼,那么可能造成讀寫(xiě)沖突冰木,如果同時(shí)修飾了讀寫(xiě)方法,則會(huì)有讀讀干擾)笼恰。此時(shí)就需要用到Lock踊沸,換言之Lock比synchronized提供了更多的功能。

使用Lock需要注意以下兩點(diǎn):

  • Lock不是語(yǔ)言內(nèi)置的社证,synchronized是Java關(guān)鍵字逼龟,為內(nèi)置特性,Lock是一個(gè)類(lèi)追葡,通過(guò)這個(gè)類(lèi)可以實(shí)現(xiàn)同步訪問(wèn)腺律。
  • 采用synchronized時(shí)我們不需要手動(dòng)去控制加鎖和釋放,系統(tǒng)會(huì)自動(dòng)控制宜肉。而使用Lock類(lèi)匀钧,我們需要手動(dòng)的加鎖和釋放,不主動(dòng)釋放可能會(huì)造成死鎖谬返。實(shí)際上Lock類(lèi)的使用某種意義上講要比synchronized更加直觀之斯。

2. Lock類(lèi)接口設(shè)計(jì)

Lock類(lèi)本身是一個(gè)接口,其方法如下:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

下面依次講解一下其中各個(gè)方法遣铝。

  • lock() 方法使用最多佑刷,作用是用于獲取鎖莉擒,如果鎖已經(jīng)被其他線程獲得,則等待瘫絮。
    通常情況下涨冀,lock使用以下方式去獲取鎖:
Lock lock = ...;
lock.lock();
try{
    //處理任務(wù)
}catch(Exception ex){
     
}finally{
    lock.unlock();   //釋放鎖
}
  • lockInterruptibly() 和lock()的區(qū)別是lockInterruptibly()鎖定的線程處于等待狀態(tài)時(shí),允許線程的打斷操作麦萤,線程使用Thread.interrupt()打斷該線程后會(huì)直接返回并拋出一個(gè)InterruptException()鹿鳖;lock()方法鎖定對(duì)象時(shí)如果在等待時(shí)檢測(cè)到線程使用Thread.interrupt(),仍然會(huì)繼續(xù)嘗試獲取鎖频鉴,失敗則繼續(xù)休眠棒坏,只是在成功獲取鎖之后在把當(dāng)前線程置為interrupt狀態(tài)验游。也就使說(shuō)陆馁,當(dāng)兩個(gè)線程同時(shí)通過(guò)lockInterruptibly()想獲取某個(gè)鎖時(shí)雷逆,假若此時(shí)線程A獲取到了鎖吧黄,而線程B只有在等待驹溃,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過(guò)程舱殿。
    因此饲宛,lockInterruptibly()方法必須實(shí)現(xiàn)catch(InterruptException e)代碼塊僵娃。常見(jiàn)使用方式如下:
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
  • tryLock() 和lock()最大的不同是具有返回值概作,或者說(shuō),它不去等待鎖默怨。如果它成功獲取鎖讯榕,那么返回true;如果它無(wú)法成功獲取鎖匙睹,則返回false愚屁。
    通常情況下,tryLock使用方式如下:
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務(wù)
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖痕檬,則直接做其他事情
}
  • tryLock(long time, TimeUnit unit) 則是介于二者之間霎槐,用戶設(shè)定一個(gè)等待時(shí)間,如果在這個(gè)時(shí)間內(nèi)獲取到了鎖梦谜,則返回true丘跌,否則返回false結(jié)束。
  • unlock() 從上面的代碼里我們也看到唁桩,unlock()一般放在異常處理操作的finally字符控制的代碼塊中闭树。我們要記得Lock和sychronized的區(qū)別,防止產(chǎn)生死鎖荒澡。
  • newCondition() 該方法我們放到后面講报辱。

3. ReentrantLock可重入鎖

3.1. ReentrantLock概述

ReentrantLock譯為“可重入鎖”,我們?cè)?a target="_blank" rel="nofollow">Java多線程:synchronized的可重入性中已經(jīng)明白了什么是可重入以及理解了synchronized的可重入性仰猖。ReentrantLock是唯一實(shí)現(xiàn)Lock接口的類(lèi)捏肢。

3.2. ReentrantLock使用

考慮到以下情景奈籽,一個(gè)僅出售雙人票的演唱會(huì)進(jìn)行門(mén)票出售,有三個(gè)售票口同時(shí)進(jìn)行售票鸵赫,買(mǎi)票需要100ms時(shí)間衣屏,每張票出票需要100ms時(shí)間。該如何設(shè)計(jì)這個(gè)情景辩棒?

package com.cielo.LockTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.lang.Thread.sleep;

/**
 * Created by 63289 on 2017/4/10.
 */
class SoldTicket implements Runnable {
    Lock lock = new ReentrantLock();//使用可重入鎖
    private volatile Integer ticket;//保證從主內(nèi)存獲取

    SoldTicket(Integer ticket) {
        this.ticket = ticket;//提供票數(shù)
    }

    private void sold() {
        lock.lock();//鎖定操作放在try代碼塊外
        try {
            if (ticket <= 0) return;//當(dāng)ticket==2時(shí)可能有多個(gè)線程進(jìn)入sold方法狼忱,一個(gè)線程運(yùn)行后另外兩個(gè)線程需要退出。
            sleep(200);//買(mǎi)票0.1s,出票0.1s
            --ticket;
            System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//獲取線程id來(lái)識(shí)別出票站一睁。
            sleep(100);//出票0.1s
            --ticket;
            System.out.println("The second ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        while (ticket > 0) {
            sold();
        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        SoldTicket soldTicket = new SoldTicket(20);
        new Thread(soldTicket).start();
        new Thread(soldTicket).start();
        new Thread(soldTicket).start();
    }
}

上面這段代碼結(jié)果如下:

The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 13, 17 tickets leave.
The second ticket is sold by 13, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 12, 13 tickets leave.
The second ticket is sold by 12, 12 tickets leave.
The first ticket is sold by 11, 11 tickets leave.
The second ticket is sold by 11, 10 tickets leave.
The first ticket is sold by 11, 9 tickets leave.
The second ticket is sold by 11, 8 tickets leave.
The first ticket is sold by 13, 7 tickets leave.
The second ticket is sold by 13, 6 tickets leave.
The first ticket is sold by 13, 5 tickets leave.
The second ticket is sold by 13, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 13, 1 tickets leave.
The second ticket is sold by 13, 0 tickets leave.

如果我們不對(duì)售票操作進(jìn)行鎖定钻弄,則會(huì)有以下幾個(gè)問(wèn)題:

  • 出售第一張票后其他機(jī)器出了另一張票,導(dǎo)致票沒(méi)有成對(duì)賣(mài)者吁。
  • 已經(jīng)無(wú)票后仍有機(jī)器出票造成混亂窘俺。

顯然,本題的情景用synchronized也可以很容易的實(shí)現(xiàn)复凳,實(shí)際上Lock有別于synchronized的主要點(diǎn)是lockInterruptibly()和tryLock()這兩個(gè)可以對(duì)鎖進(jìn)行控制的方法瘤泪。

4. ReadWriteLock讀寫(xiě)鎖

4.1. ReadWriteLock接口

回到開(kāi)頭synchronized缺陷的介紹,實(shí)際上育八,Lock接口的重要衍生接口ReadWriteLock即是解決這一問(wèn)題对途。ReadWriteLock定義很簡(jiǎn)單,僅有兩個(gè)接口:

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

即是它只提供了readLock()和writeLock()兩個(gè)操作髓棋,這兩個(gè)操作均返回一個(gè)Lock類(lèi)的實(shí)例实檀。兩個(gè)操作一個(gè)獲取讀鎖,一個(gè)獲取寫(xiě)鎖按声,將讀寫(xiě)分開(kāi)進(jìn)行操作膳犹。ReadWriteLock將讀寫(xiě)的鎖分開(kāi),可以讓多個(gè)讀操作并行儒喊,這就大大提高了效率镣奋。使用ReadWriteLock時(shí),用讀鎖去控制讀操作怀愧,寫(xiě)鎖控制寫(xiě)操作侨颈,進(jìn)而實(shí)現(xiàn)了一個(gè)可以在如下的大量讀少量寫(xiě)且讀者優(yōu)先的情景運(yùn)行的鎖。

4.2. ReentrantReadWriteLock可重入讀寫(xiě)鎖

ReentrantReadWriteLock是ReadWriteLock的唯一實(shí)例芯义。同時(shí)提供了很多操作方法哈垢。ReentratReadWriteLock接口實(shí)現(xiàn)的讀鎖寫(xiě)鎖進(jìn)入有如下要求:

4.2.1. 線程進(jìn)入讀鎖的要求

  • 沒(méi)有其他線程的寫(xiě)鎖。
  • 沒(méi)有鎖請(qǐng)求 或 調(diào)用寫(xiě)請(qǐng)求的線程正是該線程扛拨。

4.2.2. 線程進(jìn)入寫(xiě)鎖的要求

  • 沒(méi)有其他線程的讀鎖耘分。
  • 沒(méi)有其他線程的寫(xiě)鎖。

4.2.3. 讀寫(xiě)鎖使用示例

private SomeClass someClass;//資源
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//創(chuàng)建鎖
private final Lock readLock = readWriteLock.readLock();//讀鎖
private final Lock writeLock = readWriteLock.writeLock();//寫(xiě)鎖
//讀方法
readLock.lock();
try {
    result = someClass.someMethod();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    readLock.unlock();
}
//寫(xiě)方法,產(chǎn)生新的SomeClass實(shí)例tempSomeClass  
writeLock.lock();
try{
    this.someClass = tempSomeClass;//更新
}catch (Exception e) {
    e.printStackTrace();
} finally{
    writeLock.unlock();
}

5. 公平鎖

公平鎖即當(dāng)多個(gè)線程等待的一個(gè)資源的鎖釋放時(shí)求泰,線程不是隨機(jī)的獲取資源而是等待時(shí)間最久的線程獲取資源(FIFO)央渣。Java中,synchronized是一個(gè)非公平鎖渴频,無(wú)法保證鎖的獲取順序芽丹。ReentrantLock和ReentrantReadWriteLock默認(rèn)也是非公平鎖,但可以設(shè)置成公平鎖卜朗。我們前面的實(shí)例中初始化ReentrantLock和ReentrantReadWriteLock時(shí)都是無(wú)參數(shù)的拔第。實(shí)際上,它們提供一個(gè)默認(rèn)的boolean變量fair场钉,為true則為公平鎖蚊俺,為false則為非公平鎖,默認(rèn)為false逛万。因此泳猬,當(dāng)我們想將其實(shí)現(xiàn)為公平鎖時(shí),僅需要初始化時(shí)賦值true泣港。即:

    Lock lock = new ReentrantLock(true);

考慮前面賣(mài)票的實(shí)例暂殖,如果改為公平鎖(盡管這和情景無(wú)關(guān))价匠,則結(jié)果輸出非常整齊如下:

The first ticket is sold by 11, 19 tickets leave.
The second ticket is sold by 11, 18 tickets leave.
The first ticket is sold by 12, 17 tickets leave.
The second ticket is sold by 12, 16 tickets leave.
The first ticket is sold by 13, 15 tickets leave.
The second ticket is sold by 13, 14 tickets leave.
The first ticket is sold by 11, 13 tickets leave.
The second ticket is sold by 11, 12 tickets leave.
The first ticket is sold by 12, 11 tickets leave.
The second ticket is sold by 12, 10 tickets leave.
The first ticket is sold by 13, 9 tickets leave.
The second ticket is sold by 13, 8 tickets leave.
The first ticket is sold by 11, 7 tickets leave.
The second ticket is sold by 11, 6 tickets leave.
The first ticket is sold by 12, 5 tickets leave.
The second ticket is sold by 12, 4 tickets leave.
The first ticket is sold by 13, 3 tickets leave.
The second ticket is sold by 13, 2 tickets leave.
The first ticket is sold by 11, 1 tickets leave.
The second ticket is sold by 11, 0 tickets leave.

6. Lock和synchronized的選擇

  • synchronized是內(nèi)置語(yǔ)言實(shí)現(xiàn)的關(guān)鍵字当纱,Lock是為了實(shí)現(xiàn)更高級(jí)鎖功能而提供的接口。
  • synchronized發(fā)生異常時(shí)自動(dòng)釋放占有的鎖踩窖,Lock需要在finally塊中手動(dòng)釋放鎖坡氯。因此從安全性角度講,既可以用Lock又可以用synchronized時(shí)(即不需要鎖的更高級(jí)功能時(shí))使用synchronized更保險(xiǎn)洋腮。
  • 線程激烈競(jìng)爭(zhēng)時(shí)Lock的性能遠(yuǎn)優(yōu)于synchronized箫柳,即有大量線程時(shí)推薦使用Lock。
  • Lock可以通過(guò)lockInterruptibly()接口實(shí)現(xiàn)可中斷鎖啥供。
  • ReentrantReadWriteLock實(shí)現(xiàn)了封裝好的讀寫(xiě)鎖用于大量讀少量寫(xiě)讀者優(yōu)先情景解決了synchronized讀寫(xiě)情景難以實(shí)現(xiàn)問(wèn)題悯恍。

7. 參考文章

Java并發(fā)編程:Lock

lock和lockInterruptibly

說(shuō)說(shuō)ReentrantReadWriteLock

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市伙狐,隨后出現(xiàn)的幾起案子涮毫,更是在濱河造成了極大的恐慌,老刑警劉巖贷屎,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罢防,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唉侄,警方通過(guò)查閱死者的電腦和手機(jī)咒吐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人恬叹,你說(shuō)我怎么就攤上這事候生。” “怎么了绽昼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵陶舞,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我绪励,道長(zhǎng)肿孵,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任疏魏,我火速辦了婚禮停做,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘大莫。我一直安慰自己蛉腌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布只厘。 她就那樣靜靜地躺著烙丛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪羔味。 梳的紋絲不亂的頭發(fā)上河咽,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音赋元,去河邊找鬼忘蟹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛搁凸,可吹牛的內(nèi)容都是我干的媚值。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼护糖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼褥芒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嫡良,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锰扶,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后皆刺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體少辣,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年羡蛾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了漓帅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖忙干,靈堂內(nèi)的尸體忽然破棺而出器予,到底是詐尸還是另有隱情,我是刑警寧澤捐迫,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布乾翔,位于F島的核電站,受9級(jí)特大地震影響施戴,放射性物質(zhì)發(fā)生泄漏反浓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一赞哗、第九天 我趴在偏房一處隱蔽的房頂上張望雷则。 院中可真熱鬧,春花似錦肪笋、人聲如沸月劈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)猜揪。三九已至,卻和暖如春坛梁,著一層夾襖步出監(jiān)牢的瞬間而姐,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工罚勾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毅人,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓尖殃,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親划煮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子送丰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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