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

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

由于在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)度中談過,鎖會(huì)因?yàn)榈却齀/O,sleep()方法等原因被阻塞而不釋放鎖,此時(shí)如果線程還處于用synchronized修飾的代碼區(qū)域里,那么其他線程只能等待,這樣就影響了效率。因此Java提供了Lock來實(shí)現(xiàn)另一個(gè)機(jī)制,即不讓線程無限期的等待下去。

思考一個(gè)情景,當(dāng)多線程讀寫文件時(shí),讀操作和寫操作會(huì)發(fā)生沖突,寫操作和寫操作會(huì)發(fā)生沖突,但讀操作和讀操作不會(huì)有沖突。如果使用synchronized來修飾的話,就很可能造成多個(gè)讀操作無法同時(shí)進(jìn)行的可能(如果只用synchronized修飾寫方法,那么可能造成讀寫沖突,如果同時(shí)修飾了讀寫方法,則會(huì)有讀讀干擾)。此時(shí)就需要用到Lock,換言之Lock比synchronized提供了更多的功能。

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

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

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

Lock類本身是一個(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í)檢測到線程使用Thread.interrupt(),仍然會(huì)繼續(xù)嘗試獲取鎖,失敗則繼續(xù)休眠,只是在成功獲取鎖之后在把當(dāng)前線程置為interrupt狀態(tài)。也就使說,當(dāng)兩個(gè)線程同時(shí)通過lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖,而線程B只有在等待,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。
    因此,lockInterruptibly()方法必須實(shí)現(xiàn)catch(InterruptException e)代碼塊。常見使用方式如下:
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
  • tryLock() 和lock()最大的不同是具有返回值,或者說,它不去等待鎖。如果它成功獲取鎖,那么返回true;如果它無法成功獲取鎖,則返回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接口的類。

3.2. ReentrantLock使用

考慮到以下情景,一個(gè)僅出售雙人票的演唱會(huì)進(jìn)行門票出售,有三個(gè)售票口同時(shí)進(jìn)行售票,買票需要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);//買票0.1s,出票0.1s
            --ticket;
            System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//獲取線程id來識(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è)問題:

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

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

4. ReadWriteLock讀寫鎖

4.1. ReadWriteLock接口

回到開頭synchronized缺陷的介紹,實(shí)際上,Lock接口的重要衍生接口ReadWriteLock即是解決這一問題。ReadWriteLock定義很簡單,僅有兩個(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類的實(shí)例。兩個(gè)操作一個(gè)獲取讀鎖,一個(gè)獲取寫鎖,將讀寫分開進(jìn)行操作。ReadWriteLock將讀寫的鎖分開,可以讓多個(gè)讀操作并行,這就大大提高了效率。使用ReadWriteLock時(shí),用讀鎖去控制讀操作,寫鎖控制寫操作,進(jìn)而實(shí)現(xiàn)了一個(gè)可以在如下的大量讀少量寫且讀者優(yōu)先的情景運(yùn)行的鎖。

4.2. ReentrantReadWriteLock可重入讀寫鎖

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

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

  • 沒有其他線程的寫鎖。
  • 沒有鎖請(qǐng)求 或 調(diào)用寫請(qǐng)求的線程正是該線程。

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

  • 沒有其他線程的讀鎖。
  • 沒有其他線程的寫鎖。

4.2.3. 讀寫鎖使用示例

private SomeClass someClass;//資源
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//創(chuàng)建鎖
private final Lock readLock = readWriteLock.readLock();//讀鎖
private final Lock writeLock = readWriteLock.writeLock();//寫鎖
//讀方法
readLock.lock();
try {
    result = someClass.someMethod();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    readLock.unlock();
}
//寫方法,產(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è)非公平鎖,無法保證鎖的獲取順序。ReentrantLock和ReentrantReadWriteLock默認(rèn)也是非公平鎖,但可以設(shè)置成公平鎖。我們前面的實(shí)例中初始化ReentrantLock和ReentrantReadWriteLock時(shí)都是無參數(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);

考慮前面賣票的實(shí)例,如果改為公平鎖(盡管這和情景無關(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)置語言實(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)。
  • 線程激烈競爭時(shí)Lock的性能遠(yuǎn)優(yōu)于synchronized,即有大量線程時(shí)推薦使用Lock。
  • Lock可以通過lockInterruptibly()接口實(shí)現(xiàn)可中斷鎖。
  • ReentrantReadWriteLock實(shí)現(xiàn)了封裝好的讀寫鎖用于大量讀少量寫讀者優(yōu)先情景解決了synchronized讀寫情景難以實(shí)現(xiàn)問題。

7. 參考文章

Java并發(fā)編程:Lock

lock和lockInterruptibly

說說ReentrantReadWriteLock

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評(píng)論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,235評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,459評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評(píng)論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評(píng)論 2 374

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