Java并發(fā)編程之鎖機(jī)制之LockSupport工具

長鼻子.jpg

關(guān)于文章涉及到的jdk源碼,這里把最新的jdk源碼分享給大家----->jdk源碼

前言

在上篇文章《Java并發(fā)編程之鎖機(jī)制之AQS(AbstractQueuedSynchronizer)》中我們了解了整個AQS的內(nèi)部結(jié)構(gòu)替梨,與其獨占式與共享式獲取同步狀態(tài)的實現(xiàn)撬腾。但是并沒有詳細(xì)描述線程是如何進(jìn)行阻塞與喚醒的螟蝙。我也提到了線程的這些操作都與LockSupport工具類有關(guān)。現(xiàn)在我們就一起來探討一下該類的具體實現(xiàn)民傻。

LockSupport類

了解線程的阻塞和喚醒胰默,我們需要查看LockSupport類。具體代碼如下:

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    private static void setBlocker(Thread t, Object arg) {
        U.putObject(t, PARKBLOCKER, arg);
    }
    
    public static void unpark(Thread thread) {
        if (thread != null)
            U.unpark(thread);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);
        setBlocker(t, null);
    }

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            U.park(false, nanos);
            setBlocker(t, null);
        }
    }

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(true, deadline);
        setBlocker(t, null);
    }

 
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return U.getObjectVolatile(t, PARKBLOCKER);
    }

    public static void park() {
        U.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            U.park(false, nanos);
    }

    public static void parkUntil(long deadline) {
        U.park(true, deadline);
    }

    //省略部分代碼
    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long PARKBLOCKER;
    private static final long SECONDARY;
    static {
        try {
            PARKBLOCKER = U.objectFieldOffset
                (Thread.class.getDeclaredField("parkBlocker"));
            SECONDARY = U.objectFieldOffset
                (Thread.class.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

}

從上面的代碼中漓踢,我們可以知道LockSupport中的對外提供的方法都是靜態(tài)方法牵署。這些方法提供了最基本的線程阻塞和喚醒功能,在LockSupport類中定義了一組以park開頭的方法用來阻塞當(dāng)前線程喧半。以及unPark(Thread thread)方法來喚醒一個被阻塞的線程奴迅。關(guān)于park開頭的方法具體描述如下表所示:

park.png

其中park(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline)三個方法是Java 6中新增加的方法。其中參數(shù)blocker是用來標(biāo)識當(dāng)前線程等待的對象(下文簡稱為阻塞對象)挺据,該對象主要用于問題排查和系統(tǒng)監(jiān)控取具。

由于在Java 5之前脖隶,當(dāng)線程阻塞時(使用synchronized關(guān)鍵字)在一個對象上時,通過線程dump能夠查看到該線程的阻塞對象暇检。方便問題定位产阱,而Java 5退出的Lock等并發(fā)工具卻遺漏了這一點,致使在線程dump時無法提供阻塞對象的信息块仆。因此心墅,在Java 6中,LockSupport新增了含有阻塞對象的park方法榨乎。用以替代原有的park方法。

LockSupport中的blocker

可能有很多讀者對Blocker的原理有點好奇瘫筐,既然線程都被阻塞了蜜暑,是通過什么辦法將阻塞對象設(shè)置到線程中去的呢? 不急不急策肝,我們繼續(xù)查看含有阻塞對象(Object blocker)的park方法肛捍。 我們發(fā)現(xiàn)內(nèi)部都調(diào)用了setBlocker(Thread t, Object arg)方法。具體代碼如下所示:

   private static void setBlocker(Thread t, Object arg) {
        U.putObject(t, PARKBLOCKER, arg);
    }

其中 U為sun.misc.包下的Unsafe類之众。而其中的PARKBLOCKER是在靜態(tài)代碼塊中進(jìn)行賦值的拙毫,也就是如下代碼:

private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  static {
        try {
            PARKBLOCKER = U.objectFieldOffset
                (Thread.class.getDeclaredField("parkBlocker"));
           //省略部分代碼
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

Thread.class.getDeclaredField("parkBlocker")方法其實很好理解,就是獲取線程中的parkBlocker字段棺禾。如果有則返回其對應(yīng)的Field字段缀蹄,如果沒有則拋出NoSuchFieldException異常。那么關(guān)于Unsafe中的objectFieldOffset(Field f)方法怎么理解呢膘婶?

在描述該方法之前缺前,需要給大家講一個知識點。在JVM中悬襟,可以自由選擇如何實現(xiàn)Java對象的"布局",也就Java對象的各個部分分別放在內(nèi)存那個地方衅码,JVM是可以感知和決定的。 在sun.misc.Unsafe中提供了objectFieldOffset()方法用于獲取某個字段相對 Java對象的“起始地址”的偏移量,也提供了getInt脊岳、getLong逝段、getObject之類的方法可以使用前面獲取的偏移量來訪問某個Java 對象的某個字段。

有可能大家理解起來比較困難割捅,這里給大家畫了一個圖奶躯,幫助大家理解,具體如下圖所示:


blocker.png

在上圖中棺牧,我們創(chuàng)建了兩個Thread對象巫糙,其中Thread對象1在內(nèi)存中分配的地址為0x10000-0x10100,Thread對象2在內(nèi)存中分配的地址為0x11000-0x11100,其中parkBlocker對應(yīng)內(nèi)存偏移量為2(這里我們假設(shè)相對于其對象的“起始位置”的偏移量為2)。那么通過objectFieldOffset(Field f)就能獲取該字段的偏移量颊乘。需要注意的是某字段在其類中的內(nèi)存偏移量總是相同的参淹,也就是對于Thread對象1與Thread對象2醉锄,parkBlocker字段在其對象所在的內(nèi)存偏移量始終是相同的。

那么我們再回到setBlocker(Thread t, Object arg)方法浙值,當(dāng)我們獲取到parkBlocker字段在其對象內(nèi)存偏移量后恳不,
接著會調(diào)用U.putObject(t, PARKBLOCKER, arg);,該方法有三個參數(shù),第一個參數(shù)是操作對象开呐,第二個參數(shù)是內(nèi)存偏移量烟勋,第三個參數(shù)是實際存儲值。該方法理解起來也很簡單筐付,就是操作某個對象中某個內(nèi)存地址下的數(shù)據(jù)卵惦。那么結(jié)合我們上面所講的。該方法的實際操作結(jié)果如下圖所示:

blocker_set.png

到現(xiàn)在瓦戚,我們就應(yīng)該懂了沮尿,盡管當(dāng)前線程已經(jīng)阻塞,但是我們還是能直接操控線程中實際存儲該字段的內(nèi)存區(qū)域來達(dá)到我們想要的結(jié)果较解。

LockSupport底層代碼實現(xiàn)

通過閱讀源代碼我們可以發(fā)現(xiàn)畜疾,LockSupport中關(guān)于線程的阻塞和喚醒,主要調(diào)用的是sun.misc.Unsafe 中的park(boolean isAbsolute, long time)unpark(Object thread)方法印衔,也就是如下代碼:

    private static final jdk.internal.misc.Unsafe theInternalUnsafe =   
      jdk.internal.misc.Unsafe.getUnsafe();
      
    public void park(boolean isAbsolute, long time) {
        theInternalUnsafe.park(isAbsolute, time);
    }
    public void unpark(Object thread) {
        theInternalUnsafe.unpark(thread);
    }

查看sun.misc.包下的Unsafe.java文件我們可以看出啡捶,內(nèi)部其實調(diào)用的是jdk.internal.misc.Unsafe中的方法。繼續(xù)查看jdk.internal.misc.中的Unsafe.java中對應(yīng)的方法:

    @HotSpotIntrinsicCandidate
    public native void unpark(Object thread);

    @HotSpotIntrinsicCandidate
    public native void park(boolean isAbsolute, long time);

通過查看方法奸焙,我們可以得出最終調(diào)用的是JVM中的方法瞎暑,也就是會調(diào)用hotspot.share.parims包下的unsafe.cpp中的方法。繼續(xù)跟蹤与帆。

UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  //省略部分代碼
  thread->parker()->park(isAbsolute != 0, time);
  //省略部分代碼
} UNSAFE_END

UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
  Parker* p = NULL;
  //省略部分代碼
  if (p != NULL) {
    HOTSPOT_THREAD_UNPARK((uintptr_t) p);
    p->unpark();
  }
} UNSAFE_END

通過觀察代碼我們發(fā)現(xiàn)金顿,線程的阻塞和喚醒其實是與hotspot.share.runtime中的Parker類相關(guān)。我們繼續(xù)查看:

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;//該變量非常重要鲤桥,下文我們會具體描述
     //省略部分代碼
protected:
  ~Parker() { ShouldNotReachHere(); }
public:
  // For simplicity of interface with Java, all forms of park (indefinite,
  // relative, and absolute) are multiplexed into one call.
  void park(bool isAbsolute, jlong time);
  void unpark();
  //省略部分代碼

}

在上述代碼中揍拆,volatile int _counter該字段的值非常重要,一定要注意其用volatile修飾(在下文中會具體描述茶凳,接著當(dāng)我們通過SourceInsight工具(推薦大家閱讀代碼時嫂拴,使用該工具)點擊其park與unpark方法時,我們會得到如下界面:

parker.png

從圖中紅色矩形中我們可也看出贮喧,針對線程的阻塞和喚醒筒狠,不同操作系統(tǒng)有著不同的實現(xiàn)。眾所周知Java是跨平臺的箱沦。針對不同的平臺辩恼,做出不同的處理。也是非常理解的。因為作者對windows與solaris操作系統(tǒng)不是特別了解灶伊。所以這里我選擇對Linux下的平臺下進(jìn)行分析疆前。也就是選擇hotspot.os.posix包下的os_posix.cpp文件進(jìn)行分析。

Linux下的park實現(xiàn)

為了方便大家理解Linux下的阻塞實現(xiàn)聘萨,在實際代碼中我省略了一些不重要的代碼竹椒,具體如下圖所示:

void Parker::park(bool isAbsolute, jlong time) {

  //(1)如果_counter的值大于0,那么直接返回
  if (Atomic::xchg(0, &_counter) > 0) return;
    
  //獲取當(dāng)前線程
  Thread* thread = Thread::current();
  JavaThread *jt = (JavaThread *)thread;
  
  //(2)如果當(dāng)前線程已經(jīng)中斷米辐,直接返回廓潜。
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  //(3)判斷時間父能,如果時間小于0,或者在絕對時間情況下吱抚,時間為0直接返回
  struct timespec absTime;
  if (time < 0 || (isAbsolute && time == 0)) { // don't wait at all
    return;
  }
  //如果時間大于0绞愚,判斷阻塞超時時間或阻塞截止日期屑那,同時將時間賦值給absTime
  if (time > 0) {
    to_abstime(&absTime, time, isAbsolute);
  }
  //(4)如果當(dāng)前線程已經(jīng)中斷奢米,或者申請互斥鎖失敗油湖,則直接返回
  if (Thread::is_interrupted(thread, false) ||
      pthread_mutex_trylock(_mutex) != 0) {
    return;
  }

  //(5)如果是時間等于0,那么就直接阻塞線程,
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    status = pthread_cond_wait(&_cond[_cur_index], _mutex);
    assert_status(status == 0, status, "cond_timedwait");
  }
  //(6)根據(jù)absTime之前計算的時間肴捉,阻塞線程相應(yīng)時間
  else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
    assert_status(status == 0 || status == ETIMEDOUT,
                  status, "cond_timedwait");
  }
  
  //省略部分代碼
  //(7)當(dāng)線程阻塞超時,或者到達(dá)截止日期時叔收,直接喚醒線程  
  _counter = 0;
  status = pthread_mutex_unlock(_mutex);

 //省略部分代碼
}

從整個代碼來看其實關(guān)于Linux下的park方法分為以下七個步驟:

  • (1)調(diào)用Atomic::xchg方法齿穗,將_counter的值賦值為0,其方法的返回值為之前_counter的值饺律,如果返回值大于0(因為有其他線程操作過_counter的值窃页,也就是其他線程調(diào)用過unPark方法),那么就直接返回复濒。
  • (2)如果當(dāng)前線程已經(jīng)中斷脖卖,直接返回。也就是說如果當(dāng)前線程已經(jīng)中斷了巧颈,那么調(diào)用park()方法來阻塞線程就會無效畦木。
  • (3) 判斷其設(shè)置的時間是否合理,如果合理砸泛,判斷阻塞超時時間阻塞截止日期十籍,同時將時間賦值給absTime
  • (4) 在實際對線程進(jìn)行阻塞前,再一次判斷如果當(dāng)前線程已經(jīng)中斷唇礁,或者申請互斥鎖失敗勾栗,則直接返回
  • (5) 如果是時間等于0(時間為0,表示一直阻塞線程盏筐,除非調(diào)用unPark方法喚醒)围俘,那么就直接阻塞線程,
  • (6)根據(jù)absTime之前計算的時間,并調(diào)用pthread_cond_timedwait方法阻塞線程相應(yīng)的時間界牡。
  • (7) 當(dāng)線程阻塞相應(yīng)時間后簿寂,通過pthread_mutex_unlock方法直接喚醒線程,同時將_counter賦值為0。

因為關(guān)于Linux的阻塞涉及到其內(nèi)部函數(shù)欢揖,這里將用到的函數(shù)都進(jìn)行了聲明陶耍。大家可以根據(jù)下表所介紹的方法進(jìn)行理解。具體方法如下表所示:


linux方法.png

Linux下的unpark實現(xiàn)

在了解了Linux的park實現(xiàn)后她混,再來理解Linux的喚醒實現(xiàn)就非常簡單了烈钞,查看相應(yīng)方法:

void Parker::unpark() {
  int status = pthread_mutex_lock(_mutex);
  assert_status(status == 0, status, "invariant");
  const int s = _counter;
  //將_counter的值賦值為1
  _counter = 1;
  // must capture correct index before unlocking
  int index = _cur_index;
  status = pthread_mutex_unlock(_mutex);
  assert_status(status == 0, status, "invariant");
  //省略部分代碼
}

其實從代碼整體邏輯來講,最終喚醒其線程的方法為pthread_mutex_unlock(_mutex)(關(guān)于該函數(shù)的作用坤按,我已經(jīng)在上表進(jìn)行介紹了毯欣。大家可以參照Linux下的park實現(xiàn)中的圖表進(jìn)行理解)。同時將_counter的值賦值為1, 那么結(jié)合我們上文所講的park(將線程進(jìn)行阻塞)方法臭脓,那么我們可以得知整個線程的喚醒與阻塞酗钞,在Linux系統(tǒng)下,其實是受到Parker類中的_counter的值的影響的来累。

LockSupport的使用

現(xiàn)在我們基本了解了LockSupport的基本原理⊙庾鳎現(xiàn)在我們來看看它的基本使用吧。在例子中嘹锁,為了方便大家順便弄清blocker的作用葫录,這里我調(diào)用了帶blocker的park方法。具體代碼如下所示:

class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                LockSupport.park("線程a的blocker數(shù)據(jù)");
                System.out.println("我是被線程b喚醒后的操作");
            }
        });
        a.start();

        //讓當(dāng)前主線程睡眠1秒领猾,保證線程a在線程b之前執(zhí)行
        Thread.sleep(1000);
        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                
                String before = (String) LockSupport.getBlocker(a);
                System.out.println("阻塞時從線程a中獲取的blocker------>" + before);
                LockSupport.unpark(a);
                
                //這里睡眠是米同,保證線程a已經(jīng)被喚醒了
                try {
                    Thread.sleep(1000);
                    String after = (String) LockSupport.getBlocker(a);
                    System.out.println("喚醒時從線程a中獲取的blocker------>" + after);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        });
        b.start();
    }

}

代碼中,創(chuàng)建了兩個線程摔竿,線程a與線程b(線程a優(yōu)先運(yùn)行與線程b)面粮,在線程a中,通過調(diào)用LockSupport.park("線程a的blocker數(shù)據(jù)");給線程a設(shè)置了一個String類型的blocker继低,當(dāng)線程a運(yùn)行的時候熬苍,直接將線程a阻塞。在線程b中袁翁,先會獲取線程a中的blocker冷溃,打印輸出后。再通過LockSupport.unpark(a);喚醒線程a梦裂。當(dāng)喚醒線程a后似枕。最后輸出并打印線程a中的blocker。 實際代碼運(yùn)行結(jié)果如下:

阻塞時從線程a中獲取的blocker------>線程a的blocker數(shù)據(jù)
我是被線程b喚醒后的操作
喚醒時從線程a中獲取的blocker------>null

從結(jié)果中年柠,我們可以看出凿歼,線程a被阻塞時褪迟,后續(xù)就不會再進(jìn)行操作了。當(dāng)線程a被線程b喚醒后答憔。之前設(shè)置的blocker也變?yōu)閚ull了味赃。同時如果在線程a中park語句后還有額外的操作。那么會繼續(xù)運(yùn)行虐拓。關(guān)于為毛之前的blocker之前變?yōu)閚ull心俗,具體原因如下:

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);//當(dāng)線程被阻塞時,會阻塞在這里
        setBlocker(t, null);//線程被喚醒時蓉驹,會將blocer置為null
    }

通過上述例子城榛,我們完全知道了blocker可以在線程阻塞的時候,獲取數(shù)據(jù)态兴。也就證明了當(dāng)我們對線程進(jìn)行問題排查和系統(tǒng)監(jiān)控的時候blocker的有著非常重要的作用狠持。

最后

該文章參考以下博客,站在巨人的肩膀上瞻润〈梗可以看得更遠(yuǎn)。

Linux 多線程 - 線程異步與同步機(jī)制

LockSupport解析與使用

自己動手寫把”鎖”---LockSupport深入淺出

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绍撞,一起剝皮案震驚了整個濱河市正勒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌傻铣,老刑警劉巖章贞,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異矾柜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)就谜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門怪蔑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丧荐,你說我怎么就攤上這事缆瓣。” “怎么了虹统?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵弓坞,是天一觀的道長。 經(jīng)常有香客問我车荔,道長渡冻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任忧便,我火速辦了婚禮族吻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己超歌,他們只是感情好砍艾,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著巍举,像睡著了一般脆荷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懊悯,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天蜓谋,我揣著相機(jī)與錄音,去河邊找鬼定枷。 笑死孤澎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的欠窒。 我是一名探鬼主播覆旭,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼岖妄!你這毒婦竟也來了型将?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤荐虐,失蹤者是張志新(化名)和其女友劉穎七兜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體福扬,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡腕铸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了铛碑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狠裹。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汽烦,靈堂內(nèi)的尸體忽然破棺而出涛菠,到底是詐尸還是另有隱情,我是刑警寧澤撇吞,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布俗冻,位于F島的核電站,受9級特大地震影響牍颈,放射性物質(zhì)發(fā)生泄漏迄薄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一煮岁、第九天 我趴在偏房一處隱蔽的房頂上張望噪奄。 院中可真熱鬧死姚,春花似錦、人聲如沸勤篮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽碰缔。三九已至账劲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間金抡,已是汗流浹背瀑焦。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留梗肝,地道東北人榛瓮。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像巫击,于是被迫代替她去往敵國和親禀晓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

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