0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Lock與Condition接口條件變量方式

科技綠洲 ? 來(lái)源:Java技術(shù)指北 ? 作者:Java技術(shù)指北 ? 2023-10-13 11:21 ? 次閱讀

我們知道,并發(fā)領(lǐng)域中有兩大核心問(wèn)題:互斥與同步問(wèn)題,Java在1.5版本之前,是提供了synchronized來(lái)實(shí)現(xiàn)的。synchronized是內(nèi)置鎖,雖然在大部分情況下它都能很好的工作,但是依然還是會(huì)存在一些局限性,除了當(dāng)時(shí)1.5版本的性能問(wèn)題外(1.6版本后,synchronized的性能已經(jīng)得到了很大的優(yōu)化),還有如下兩個(gè)問(wèn)題:

  1. 無(wú)法解決死鎖問(wèn)題
  2. 最多使用一個(gè)條件變量

所以針對(duì)這些問(wèn)題,Doug Lea在并發(fā)包中增加了兩個(gè)接口Lock和Condition來(lái)解決這兩個(gè)問(wèn)題,所以今天就說(shuō)說(shuō)這兩個(gè)接口是如何解決synchronized中的這兩個(gè)問(wèn)題的。

一. Lock接口

1.1 介紹

在我們分析Lock接口是如何解決死鎖問(wèn)題之前,我們先看看死鎖是如何產(chǎn)生的。死鎖的產(chǎn)生需要滿足下面四個(gè)條件:

  1. 互斥 :共享資源同一時(shí)間只能被一個(gè)線程占用
  2. 不可搶占 :其他線程不能強(qiáng)行占有另一個(gè)線程的資源
  3. 占有且等待 :線程在等待其他資源時(shí),不釋放自己已占有的資源
  4. 循環(huán)等待 :線程1和線程2互相占有對(duì)方的資源并相互等待

所以,我們只需要破壞上面條件中的任意一個(gè),即可打破死鎖。但需要注意的是,互斥條件是不能破壞的,因?yàn)槭褂面i的目的就是為了互斥。所以Lock接口通過(guò)破壞掉 "不可搶占"這個(gè)條件來(lái)解決死鎖,具體如下:

  1. 非阻塞獲取鎖 :嘗試獲取鎖,如果失敗了就立刻返回失敗,這樣就可以釋放已經(jīng)持有的其他鎖
  2. 響應(yīng)中斷 :如果發(fā)生死鎖后,此線程被其他線程中斷,則會(huì)釋放鎖,解除死鎖
  3. 支持超時(shí) :一段時(shí)間內(nèi)獲取不到鎖,就返回失敗,這樣就可以釋放之前已經(jīng)持有的鎖

接下來(lái)我們具體看看接口代碼吧。

1.2 源碼解讀

public interface Lock {
    /**
        阻塞獲取鎖,不響應(yīng)中斷,如果獲取不到,則當(dāng)前線程將進(jìn)入休眠狀態(tài),直到獲得鎖為止。
    */
    void lock();

    /**
        阻塞獲取鎖,響應(yīng)中斷,如果出現(xiàn)以下兩種情況將拋出異常
        1.調(diào)用該方法時(shí),此線程中斷標(biāo)志位被設(shè)置為true
        2.獲取鎖的過(guò)程中此線程被中斷,并且獲取鎖的實(shí)現(xiàn)會(huì)響應(yīng)中斷
    */
    void lockInterruptibly() throws InterruptedException;
  
    /**
        非阻塞獲取鎖,不管成功還是失敗,都會(huì)立刻返回結(jié)果,成功了返回true,失敗了返回false
     */
    boolean tryLock();
 
    /**
      帶超時(shí)時(shí)間且響應(yīng)中斷的獲取鎖,如果獲取鎖成功,則返回true,獲取不到則會(huì)休眠,直到下面三個(gè)條件滿足
      1.當(dāng)前線程獲取到鎖
      2.其他線程中斷了當(dāng)前線程,并且獲取鎖的實(shí)現(xiàn)支持中斷
      3.設(shè)置的超時(shí)事件到了
      而拋出異常的情況與lockInterruptibly一致
      當(dāng)異常拋出后中斷標(biāo)志位會(huì)被清除,且超時(shí)時(shí)間到了,當(dāng)前線程還沒(méi)有獲得鎖,則會(huì)直接返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
   
    /**
        沒(méi)啥好說(shuō),只有擁有鎖的線程才能釋放鎖
     */
    void unlock();

    /**
        返回綁定到此Lock實(shí)例的新Condition實(shí)例。
        在等待該條件之前,該鎖必須由當(dāng)前線程持有。調(diào)用Condition.await()會(huì)在等待之前自動(dòng)釋放鎖,并在等待返回之前重新獲取該鎖。
        我們?cè)傧乱恍」?jié)再詳細(xì)說(shuō)說(shuō)Condition接口
     */
    Condition newCondition();
}

還需要額外注意的一點(diǎn),使用synchronized作為鎖時(shí),我們是不需要考慮釋放鎖的,但Lock是屬于顯示鎖,是需要我們手動(dòng)釋放鎖的。我們一般在finally塊中調(diào)用lock.unlock()手動(dòng)釋放鎖,具體形式如下:

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

我們最后通過(guò)一張圖來(lái)總結(jié)下Lock接口:

圖片

二. Condition接口

2.1 介紹

針對(duì)synchronized最多只能使用一個(gè)條件變量的問(wèn)題,Condition接口提供了解決方案。但是為什么多個(gè)條件變量就比一個(gè)條件變量好呢?我們先來(lái)看看synchronized使用一個(gè)條件變量時(shí)會(huì)有什么弊端。

一個(gè)synchronized內(nèi)置鎖只對(duì)應(yīng)一個(gè)等待容器(wait set),當(dāng)線程調(diào)用wait方法時(shí),會(huì)把當(dāng)前線程放入到同一個(gè)等待容器中,當(dāng)我們需要根據(jù)某些特定的條件來(lái)喚醒符合條件的線程時(shí),我們只能先從等待容器里喚醒一個(gè)線程后,再看是否符合條件。如果不符合條件,則需要將此線程繼續(xù)wait,然后再去等待容器中獲取下一個(gè)線程再判斷是否滿足條件。這樣會(huì)導(dǎo)致許多無(wú)意義的cpu開(kāi)銷。

我們可以看到Lock接口中有個(gè)newCondition()的方法:

Condition newCondition();

通過(guò)這個(gè)方法,一個(gè)鎖可以建立多個(gè)Conditiion,每個(gè)Condtition都有一個(gè)容器來(lái)保存相應(yīng)的等待線程,拿到鎖的線程根據(jù)特定的條件喚醒對(duì)應(yīng)的線程時(shí),只需要去喚醒對(duì)應(yīng)的Contition內(nèi)置容器中的線程即可,這樣就可以減少無(wú)意義的CPU開(kāi)銷。然后我們具體看看Condition接口的源碼。

2.2 源碼解讀

public interface Condition {

    /**
 使當(dāng)前線程等待,并響應(yīng)中斷。當(dāng)當(dāng)前線程進(jìn)入休眠狀態(tài)后,如果發(fā)生以下四種情況將會(huì)被喚醒:
 1.其他一些線程對(duì)此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
 2.其他一些線程對(duì)此條件調(diào)用signalAll方法
 3.其他一些線程中斷當(dāng)前線程,并支持中斷線程掛起
 4.發(fā)生“虛假喚醒”。
     */
    void await() throws InterruptedException;

    /**
 使當(dāng)前線程等待,并不響應(yīng)中斷。只有以下三種情況才會(huì)被喚醒
 1.其他一些線程對(duì)此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
 2.其他一些線程對(duì)此條件調(diào)用signalAll方法
 3.發(fā)生“虛假喚醒”。
     */
    void awaitUninterruptibly();

    /**
        使當(dāng)前線程等待,響應(yīng)中斷,且可以指定超時(shí)事件。發(fā)生以下五種情況之一將會(huì)被喚醒:
        1.其他一些線程為此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
        2.其他一些線程為此條件調(diào)用signalAll方法;
        3.其他一些線程中斷當(dāng)前線程,并且支持中斷線程掛起;
        4.經(jīng)過(guò)指定的等待時(shí)間;
        5.發(fā)生“虛假喚醒”。
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    /**
 與awaitNanos類似,時(shí)間單位不同
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /**
 與awaitNanos類似,只不過(guò)超時(shí)時(shí)間是截止時(shí)間
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;

    /**
 喚醒一個(gè)等待線程
     */
    void signal();

    /**
 喚醒所有等待線程
     */
    void signalAll();
}

需要注意的是,Object類的等待方法是沒(méi)有返回值的,但Condtition類中的部分等待方法是有返回值的。awaitNanos(long nanosTimeout)返回了剩余等待的時(shí)間;await(long time, TimeUnit unit)返回boolean值,如果返回false,則說(shuō)明是因?yàn)槌瑫r(shí)返回的,否則返回true。為什么增加返回值?為了就是幫助我們弄清楚方法返回的原因。

四. 阿里多線程考題

最后我們通過(guò)實(shí)現(xiàn)了Lock和Condition接口能力的ReentrantLock類來(lái)解決阿里多線程面試題。

題目是使用三個(gè)線程循環(huán)打印ABC,一共打印50次。我們直接上答案:

public class Test {


    int count = 0;
    Lock lock = new ReentrantLock();
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();

    public void printA() {
        while (count < 50) {
            try {
                // 加鎖
                lock.lock();
                // 打印A
                System.out.println("A");
                count ++;
                // 喚醒打印B的線程
                conditionB.signal();
                // 將自己放入ConditionA的容器中,等待其他線程的喚醒
                conditionA.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 釋放鎖
                lock.unlock();
            }
        }


    }

    public void printB() {
        while (count < 50) {
            try {
                // 加鎖
                lock.lock();
                // 打印B
                System.out.println("B");
                count ++;
                // 喚醒打印C的線程
                conditionC.signal();
                // 將自己放入ConditionB的容器中,等待其他線程的喚醒
                conditionB.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 釋放鎖
                lock.unlock();
            }
        }
    }


    public void printC() {
        while (count < 50) {
            try {
                // 加鎖
                lock.lock();
                // 打印B
                System.out.println("C");
                count ++;
                // 喚醒打印A的線程
                conditionA.signal();
                // 將自己放入ConditionC的容器中,等待其他線程的喚醒
                conditionC.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        // 建立打印ABC的三個(gè)線程
        Thread theadA = new Thread(() - > {
            test.printA();
        });
        Thread theadB = new Thread(() - > {
            test.printB();
        });
        Thread theadC = new Thread(() - > {
            test.printC();
        });
    
        // 啟動(dòng)線程
        theadA.start();
        theadB.start();
        theadC.start();

    }
}

五. 總結(jié)

Lock與Condition接口就說(shuō)完了,最后再總結(jié)一下:

針對(duì)synchronized內(nèi)置鎖無(wú)法解決死鎖、只有一個(gè)條件變量等問(wèn)題,Doug Lea在Java并發(fā)包中增加了Lock和Condition接口來(lái)解決。對(duì)于死鎖問(wèn)題,Lock接口增加了超時(shí)、響應(yīng)中斷、非阻塞三種方式來(lái)獲取鎖,從而避免了死鎖。針對(duì)一個(gè)條件變量問(wèn)題,Condtition接口通過(guò)一把鎖可以創(chuàng)建多個(gè)條件變量的方式來(lái)解決。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    8451

    瀏覽量

    150732
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2952

    瀏覽量

    104489
  • Lock
    +關(guān)注

    關(guān)注

    0

    文章

    10

    瀏覽量

    7752
  • 線程
    +關(guān)注

    關(guān)注

    0

    文章

    504

    瀏覽量

    19636
收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Linux下線程間通訊---讀寫鎖和條件變量

    讀寫鎖,它把對(duì)共享資源的訪問(wèn)者劃分成讀者和寫者,讀者只對(duì)共享資源進(jìn)行讀訪問(wèn),寫者則需要對(duì)共享資源進(jìn)行寫操作。件變量是線程可用的一種同步機(jī)制,條件變量給多個(gè)線程提供了一個(gè)回合的場(chǎng)所,條件
    的頭像 發(fā)表于 08-26 20:44 ?1419次閱讀
    Linux下線程間通訊---讀寫鎖和<b class='flag-5'>條件</b><b class='flag-5'>變量</b>

    Linux系統(tǒng)中線程同步方式中的條件變量方法

    今天主要和大家聊一聊,如何使用Linux中線程同步方式中的條件變量。
    發(fā)表于 11-08 09:16 ?505次閱讀

    labview枚舉變量條件結(jié)構(gòu)的結(jié)合的問(wèn)題

    如圖:程序運(yùn)行時(shí),為什么在前面板上改變枚舉變量的值,條件結(jié)構(gòu)的條件中對(duì)應(yīng)的沒(méi)有改變呢?
    發(fā)表于 12-06 15:21

    Raw condition msg 篇

    調(diào)用這個(gè)函數(shù)會(huì)立即block 在condition msg上,直到其他任務(wù)調(diào)用raw_cond_msg_set,滿足條件后才會(huì)醒過(guò)來(lái)并接收到一個(gè)消息。Wait_option 可以設(shè)置為
    發(fā)表于 02-27 14:08

    條件結(jié)構(gòu)的布爾變量問(wèn)題?

    本帖最后由 dsl7410 于 2016-1-13 00:00 編輯 這個(gè)順序結(jié)構(gòu)里面的條件結(jié)構(gòu)一個(gè)是把value值+1,一個(gè)是-1,+1可以理解,因?yàn)榉种нx擇器接了加的布爾變量,但是減的那個(gè)分支怎么實(shí)現(xiàn)呢?分支選擇器沒(méi)有連接減啊,那個(gè)減的布爾控件刪除了,還能實(shí)現(xiàn)-
    發(fā)表于 01-12 23:57

    Linux C 多線程編程之互斥鎖與條件變量實(shí)例詳解

    。這時(shí)線程掛起,不占用 CPU 時(shí)間,直到條件變量被觸發(fā)。因此,全過(guò)程可以描述為:(1)pthread_mutex_lock()上鎖,(2)pthread_cond_wait()等待,等待過(guò)程分解為為
    發(fā)表于 06-03 17:13

    淺析linux下的條件變量

    變量中常用的API: ? ? ?1).條件變量類型為:pthread_cond_t ,類似互斥變量,條件
    發(fā)表于 07-12 08:10

    Lock體系結(jié)構(gòu)和讀寫鎖機(jī)制解析

    排它性,即同一個(gè)時(shí)刻只有一個(gè)線程進(jìn)入任務(wù)。Condition接口Condition接口描述可能會(huì)與鎖有關(guān)聯(lián)的條件
    發(fā)表于 01-05 17:53

    【隨筆記】C++ condition_variable 陷阱

    ; lock(mutex_data_); cond_.notify_all(); } 改進(jìn)方案一(使用 select 方式實(shí)現(xiàn)):缺點(diǎn)是一個(gè)對(duì)象會(huì)浪費(fèi)兩個(gè)文件描述符資源 DelayControl
    發(fā)表于 11-24 10:41

    FIDIC合同條件體系及應(yīng)用方式

    介紹了FIDIC組織、FIDIC合同條件體系及其最新發(fā)展,討論了FIDIC合同條件的應(yīng)用方式,強(qiáng)調(diào)了學(xué)習(xí)FIDIC合同條件的意義和重要性。
    發(fā)表于 01-08 15:32 ?2次下載

    linux設(shè)置環(huán)境變量的三種方式

     linux設(shè)置環(huán)境變量有以下三種方式
    發(fā)表于 06-15 09:05 ?1399次閱讀
    linux設(shè)置環(huán)境<b class='flag-5'>變量</b>的三種<b class='flag-5'>方式</b>

    詳談Linux操作系統(tǒng)編程的條件變量

    條件變量是用來(lái)等待線程而不是上鎖的,條件變量通常和互斥鎖一起使用。條件變量之所以要和互斥鎖一起使
    的頭像 發(fā)表于 09-27 15:23 ?1962次閱讀
    詳談Linux操作系統(tǒng)編程的<b class='flag-5'>條件</b><b class='flag-5'>變量</b>

    TensorRT條件用于實(shí)現(xiàn)網(wǎng)絡(luò)子圖的條件執(zhí)行

    IIfConditional實(shí)現(xiàn)了一個(gè) if-then-else 流控制結(jié)構(gòu),該結(jié)構(gòu)提供基于動(dòng)態(tài)布爾輸入的網(wǎng)絡(luò)子圖的條件執(zhí)行。它由一個(gè)布爾標(biāo)量predicate condition和兩個(gè)分支子圖定義
    的頭像 發(fā)表于 05-18 10:02 ?1128次閱讀

    Linux線程條件變量是什么意思

    條件變量 條件變量用于自動(dòng)阻塞線程,直到某個(gè)特定事件發(fā)生或某個(gè)條件滿足為止,通常情況下,條件
    的頭像 發(fā)表于 07-21 11:18 ?478次閱讀

    case怎么使用多個(gè)條件

    在編寫代碼時(shí),我們經(jīng)常需要根據(jù)不同的條件來(lái)執(zhí)行不同的操作。在Python中,我們可以使用 if 語(yǔ)句來(lái)實(shí)現(xiàn)這一目的。 if 語(yǔ)句允許我們?cè)O(shè)置多個(gè)條件,并且根據(jù)不同的條件執(zhí)行不同的代碼塊。 語(yǔ)法結(jié)構(gòu)
    的頭像 發(fā)表于 11-30 14:34 ?1122次閱讀