自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 線程的狀態(tài)及轉(zhuǎn)換

開發(fā) 后端
將弱化 threadStatus 這個(gè)整數(shù)值了,就直接說改變了其線程狀態(tài),大家知道其實(shí)就只是改變了 threadStatus 的值而已。

低并發(fā)編程

戰(zhàn)略上藐視技術(shù),戰(zhàn)術(shù)上重視技術(shù)

閃客:小宇你怎么了,我看你臉色很不好呀。

小宇:今天去面試了,面試官問我 Java 線程的狀態(tài)及其轉(zhuǎn)化。

閃客:哦哦,很常見的面試題呀,不是有一張狀態(tài)流轉(zhuǎn)圖嘛。

小宇:我知道,可是我每次面試的時(shí)候,腦子里記過的流轉(zhuǎn)圖就變成這樣了。


閃客:哈哈哈。

小宇:你還笑,氣死我了,你能不能給我講講這些亂七八糟的狀態(tài)呀。

閃客:沒問題,還是老規(guī)矩,你先把所有狀態(tài)都忘掉,聽我從頭道來!

小宇:好滴。

線程狀態(tài)的實(shí)質(zhì)

首先你得明白,當(dāng)我們說一個(gè)線程的狀態(tài)時(shí),說的是什么?

沒錯(cuò),就是一個(gè)變量的值而已。

哪個(gè)變量?

Thread 類中的一個(gè)變量,叫

private volatile int threadStatus = 0;

這個(gè)值是個(gè)整數(shù),不方便理解,可以通過映射關(guān)系(VM.toThreadState),轉(zhuǎn)換成一個(gè)枚舉類。

public enum State {

NEW,

RUNNABLE,

BLOCKED,

WAITING,

TIMED_WAITING,

TERMINATED;

}

所以,我們就盯著 threadStatus 這個(gè)值的變化就好了。

就是這么簡單。

NEW

現(xiàn)在我們還沒有任何 Thread 類的對象呢,也就不存在線程狀態(tài)一說。

一切的起點(diǎn),要從把一個(gè) Thread 類的對象創(chuàng)建出來,開始說起。

Thread t = new Thread();

當(dāng)然,你后面可以接很多參數(shù)。

Thread t = new Thread(r, "name1");

你也可以 new 一個(gè)繼承了 Thread 類的子類。

Thread t = new MyThread();

你說線程池怎么不 new 就可以有線程了呢?人家內(nèi)部也是 new 出來的。

public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread();
return t;
}
}
public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread();
return t;
}
}
public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread();
return t;
}
}
}

總是,一切的開始,都要調(diào)用 Thread 類的構(gòu)造方法。

而這個(gè)構(gòu)造方法,最終都會調(diào)用 Thread 類的 init () 方法。

private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
this.grout = g;
this.name = name;
tid = nextThreadID();
}

這個(gè) init 方法,僅僅是給該 Thread 類的對象中的屬性,附上值,除此之外啥也沒干。

它沒有給 theadStatus 再次賦值,所以它的值仍然是其默認(rèn)值。

而這個(gè)值對應(yīng)的狀態(tài),就是 STATE.NEW,非要翻譯成中文,就叫初始態(tài)吧。

因此說了這么多,其實(shí)就分析出了,新建一個(gè) Thread 類的對象,就是創(chuàng)建了一個(gè)新的線程,此時(shí)這個(gè)線程的狀態(tài),是 NEW(初始態(tài))。

之后的分析,將弱化 threadStatus 這個(gè)整數(shù)值了,就直接說改變了其線程狀態(tài),大家知道其實(shí)就只是改變了 threadStatus 的值而已。

RUNNABLE

你說,剛剛處于 NEW 狀態(tài)的線程,對應(yīng)操作系統(tǒng)里的什么狀態(tài)呢?

一看你就沒仔細(xì)看我上面的分析。

Thread t = new Thread();

只是做了些表面功夫,在 Java 語言層面將自己的一個(gè)對象中的屬性附上值罷了,根本沒碰到操作系統(tǒng)級別的東西呢。

所以這個(gè) NEW 狀態(tài),不論往深了說還是往淺了說,還真就只是個(gè)無聊的枚舉值而已。

下面,精彩的故事才剛剛開始。

躺在堆內(nèi)存中無所事事的 Thread 對象,在調(diào)用了 start () 方法后,才顯現(xiàn)生機(jī)。

t.start();

這個(gè)方法一調(diào)用,那可不得了,最終會調(diào)用到一個(gè)討厭的 native 方法里。

private native void start0();

看來改變狀態(tài)就并不是一句 threadStatus = xxx 這么簡單了,而是有本地方法對其進(jìn)行了修改。

九曲十八彎跟進(jìn) jvm 源碼之后,調(diào)用到了這個(gè)方法。

hotspot/src/os/linux/vm/os_linux.cpp
pthread_create();

大名鼎鼎的 unix 創(chuàng)建線程的方法,pthread_create。

此時(shí),在操作系統(tǒng)內(nèi)核中,才有了一個(gè)真正的線程,被創(chuàng)建出來。

而 linux 操作系統(tǒng),是沒有所謂的剛創(chuàng)建但沒啟動的線程這種說法的,創(chuàng)建即刻開始運(yùn)行。

雖然無法從源碼發(fā)現(xiàn)線程狀態(tài)的變化,但通過 debug 的方式,我們看到調(diào)用了 Thread.start () 方法后,線程的狀態(tài)變成了 RUNNABLE,運(yùn)行態(tài)。

那我們的狀態(tài)圖又豐富了起來。


通過這部分,我們知道如下幾點(diǎn):

1. 在 Java 調(diào)用 start () 后,操作系統(tǒng)中才真正出現(xiàn)了一個(gè)線程,并且立刻運(yùn)行。

2. Java 中的線程,和操作系統(tǒng)內(nèi)核中的線程,是一對一的關(guān)系。

3. 調(diào)用 start 后,線程狀態(tài)變?yōu)?RUNNABLE,這是由 native 方法里的某部分代碼造成的。

RUNNING 和 READY

CPU 一個(gè)核心,同一時(shí)刻,只能運(yùn)行一個(gè)線程。

具體執(zhí)行哪個(gè)線程,要看操作系統(tǒng) 的調(diào)度機(jī)制。

所以,上面的 RUNNABLE 狀態(tài),準(zhǔn)確說是,得到了可以隨時(shí)準(zhǔn)備運(yùn)行的機(jī)會的狀態(tài)。

而處于這個(gè)狀態(tài)中的線程,也分為了正在 CPU 中運(yùn)行的線程,和一堆處于就緒中等待 CPU 分配時(shí)間片來運(yùn)行的線程。

處于就緒中的線程,會存儲在一個(gè)就緒隊(duì)列中,等待著被操作系統(tǒng)的調(diào)度機(jī)制選到,進(jìn)入 CPU 中運(yùn)行。

當(dāng)然,要注意,這里的 RUNNING 和 READY 狀態(tài),是我們自己為了方便描述而造出來的。

無論是 Java 語言,還是操作系統(tǒng),都不區(qū)分這兩種狀態(tài),在 Java 中統(tǒng)統(tǒng)叫 RUNNABLE。

TERMINATED

當(dāng)一個(gè)線程執(zhí)行完畢(或者調(diào)用已經(jīng)不建議的 stop 方法),線程的狀態(tài)就變?yōu)?TERMINATED。

此時(shí)這個(gè)線程已經(jīng)無法死灰復(fù)燃了,如果你此時(shí)再強(qiáng)行執(zhí)行 start 方法,將會報(bào)出錯(cuò)誤。

java.lang.IllegalThreadStateException

很簡單,因?yàn)?start 方法的第一行就是這么直戳了當(dāng)?shù)貙懙摹?/p>

public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
...
}

誒,那如果此時(shí)強(qiáng)行把 threadStatus 改成 0,會怎么樣呢?你可以試試喲。

BLOCKED

上面把最常見,最簡單的線程生命周期講完了。

初始 -- 運(yùn)行 -- 終止

沒有發(fā)生任何的障礙。

接下來,就稍稍復(fù)雜一點(diǎn)了,我們讓線程碰到些障礙。

首先創(chuàng)建一個(gè)對象 lock。

public static final Object lock = new Object();

一個(gè)線程,執(zhí)行一個(gè) sychronized 塊,鎖對象是 lock,且一直持有這把鎖不放。

new Thread(() - {
synchronized (lock) {
while(true) {}
}
}).start();

另一個(gè)線程,也同樣執(zhí)行一個(gè)鎖對象為 lock 的 sychronized 塊。

new Thread(() - {
synchronized (lock) {
...
}
}).start();

那么,在進(jìn)入 synchronized 塊時(shí),因?yàn)闊o法拿到鎖,會使線程狀態(tài)變?yōu)?BLOCKED。

同樣,對于 synchronized 方法,也是如此。

當(dāng)該線程獲取到了鎖后,便可以進(jìn)入 synchronized 塊,此時(shí)線程狀態(tài)變?yōu)?RUNNABLE。

因此我們得出如下轉(zhuǎn)換關(guān)系。

當(dāng)然,這只是線程狀態(tài)的改變,線程還發(fā)生了一些實(shí)質(zhì)性的變化。

我們不考慮虛擬機(jī)對 synchronized 的極致優(yōu)化。

當(dāng)進(jìn)入 synchronized 塊或方法,獲取不到鎖時(shí),線程會進(jìn)入一個(gè)該鎖對象的同步隊(duì)列。

當(dāng)持有鎖的這個(gè)線程,釋放了鎖之后,會喚醒該鎖對象同步隊(duì)列中的所有線程,這些線程會繼續(xù)嘗試搶鎖。如此往復(fù)。

比如,有一個(gè)鎖對象 A,線程 1 此時(shí)持有這把鎖。線程 2、3、4 分別嘗試搶這把鎖失敗。


線程 1 釋放鎖,線程 2、3、4 重新變?yōu)?RUNNABLE,繼續(xù)搶鎖,假如此時(shí)線程 3 搶到了鎖。

如此往復(fù)。

WAITING

這部分是最復(fù)雜的,同時(shí)也是面試中考點(diǎn)最多的,將分成三部分講解。聽我說完后你會發(fā)現(xiàn),這三部分有很多相同但地方,不再是孤立的知識點(diǎn)。

wait/notify

我們在剛剛的 synchronized 塊中加點(diǎn)東西。

new Thread(() - {
synchronized (lock) {
...
lock.wait();
...
}
}).start();

當(dāng)這個(gè) lock.wait () 方法一調(diào)用,會發(fā)生三件事。

1. 釋放鎖對象 lock(隱含著必須先獲取到這個(gè)鎖才行)

2. 線程狀態(tài)變成 WAITING

3. 線程進(jìn)入 lock 對象的等待隊(duì)列


什么時(shí)候這個(gè)線程被喚醒,從等待隊(duì)列中移出,并從 WAITING 狀態(tài)返回 RUNNABLE 狀態(tài)呢?

必須由另一個(gè)線程,調(diào)用同一個(gè)對象的 notify / notifyAll 方法。

new Thread(() - {
synchronized (lock) {
...
lock.notify();
...
}
}).start();

只不過 notify 是只喚醒一個(gè)線程,而 notifyAll 是喚醒所有等待隊(duì)列中的線程。

但需要注意,被喚醒后的線程,從等待隊(duì)列移出,狀態(tài)變?yōu)?RUNNABLE,但仍然需要搶鎖,搶鎖成功了,才可以從 wait 方法返回,繼續(xù)執(zhí)行。

如果失敗了,就和上一部分的 BLOCKED 流程一樣了。


所以我們的整個(gè)流程圖,現(xiàn)在變成了這個(gè)樣子。


join

主線程這樣寫。

public static void main(String[] args) {
thread t = new Thread();
t.start();
t.join();
}

當(dāng)執(zhí)行到 t.join () 的時(shí)候,主線程會變成 WAITING 狀態(tài),直到線程 t 執(zhí)行完畢,主線程才會變回 RUNNABLE 狀態(tài),繼續(xù)往下執(zhí)行。

看起來,就像是主線程執(zhí)行過程中,另一個(gè)線程插隊(duì)加入(join),而且要等到其結(jié)束后主線程才繼續(xù)。

因此我們的狀態(tài)圖,又多了兩項(xiàng)。

那 join 又是怎么神奇地實(shí)現(xiàn)這一切呢?也是像 wait 一樣放到等待隊(duì)列么?

打開 Thread.join () 的源碼,你會發(fā)現(xiàn)它非常簡單。

// Thread.java
// 無參的 join 有用的信息就這些,省略了額外分支
public synchronized void join() {
while (isAlive()) {
wait();
}
}

也就是說,他的本質(zhì)仍然是執(zhí)行了 wait () 方法,而鎖對象就是 Thread t 對象本身。

那從 RUNNABLE 到 WAITING,就和執(zhí)行了 wait () 方法完全一樣了。

那從 WAITING 回到 RUNNABLE 是怎么實(shí)現(xiàn)的呢?

主線程調(diào)用了 wait ,需要另一個(gè)線程 notify 才行,難道需要這個(gè)子線程 t 在結(jié)束之前,調(diào)用一下 t.notifyAll () 么?

答案是否定的,那就只有一種可能,線程 t 結(jié)束后,由 jvm 自動調(diào)用 t.notifyAll (),不用我們程序顯示寫出。

沒錯(cuò),就是這樣。

怎么證明這一點(diǎn)呢?道聽途說可不行,老子今天非要扒開 jvm 的外套。

果然,找到了如下代碼。

hotspot/src/share/vm/runtime/thread.cpp
void JavaThread::exit(...) {
...
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
...
lock.notify_all(thread);
...
}

我們看到,虛擬機(jī)在一個(gè)線程的方法執(zhí)行完畢后,執(zhí)行了個(gè) ensure_join 方法,看名字就知道是專門為 join 而設(shè)計(jì)的。

而繼續(xù)跟進(jìn)會發(fā)現(xiàn)一段關(guān)鍵代碼,lock.notify_all,這便是一個(gè)線程結(jié)束后,會自動調(diào)用自己的 notifyAll 方法的證明。

所以,其實(shí) join 就是 wait,線程結(jié)束就是 notifyAll。現(xiàn)在,是不是更清晰了。


park/unpark

有了上面 wait 和 notify 的機(jī)制,下面就好理解了。

一個(gè)線程調(diào)用如下方法。

LockSupport.park()

該線程狀態(tài)會從 RUNNABLE 變成 WAITING、

另一個(gè)線程調(diào)用

LockSupport.unpark (Thread 剛剛的線程)

剛剛的線程會從 WAITING 回到 RUNNABLE

但從線程狀態(tài)流轉(zhuǎn)來看,與 wait 和 notify 相同。

從實(shí)現(xiàn)機(jī)制上看,他們甚至更為簡單。

1. park 和 unpark 無需事先獲取鎖,或者說跟鎖壓根無關(guān)。

2. 沒有什么等待隊(duì)列一說,unpark 會精準(zhǔn)喚醒某一個(gè)確定的線程。

3. park 和 unpark 沒有順序要求,可以先調(diào)用 unpark

關(guān)于第三點(diǎn),就涉及到 park 的原理了,這里我只簡單說明。

線程有一個(gè)計(jì)數(shù)器,初始值為 0

調(diào)用 park 就是

如果這個(gè)值為 0,就將線程掛起,狀態(tài)改為 WAITING。如果這個(gè)值為 1,則將這個(gè)值改為 0,其余的什么都不做。

調(diào)用 unpark 就是

將這個(gè)值改為 1

然后我用三個(gè)例子,你就基本明白了。

// 例子1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以運(yùn)行到這");
// 例子2
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以運(yùn)行到這");
// 例子3
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
LockSupport.park(); // WAITING
System.out.println("不可以運(yùn)行到這");

park 的使用非常簡單,同時(shí)也是 JDK 中鎖實(shí)現(xiàn)的底層。它的 JVM 及操作系統(tǒng)層面的原理很復(fù)雜,改天可以專門找一節(jié)來講解。

現(xiàn)在我們的狀態(tài)圖,又可以更新了。


TIMED_WAITING

這部分就再簡單不過了,將上面導(dǎo)致線程變成 WAITING 狀態(tài)的那些方法,都增加一個(gè)超時(shí)參數(shù),就變成了將線程變成 TIMED_WAITING 狀態(tài)的方法了,我們直接更新流程圖。

這些方法的唯一區(qū)別就是,從 TIMED_WAITING 返回 RUNNABLE,不但可以通過之前的方式,還可以通過到了超時(shí)時(shí)間,返回 RUNNABLE 狀態(tài)。

就這樣。

還有,大家看。

wait 需要先獲取鎖,再釋放鎖,然后等待被 notify。

join 就是 wait 的封裝。

park 需要等待 unpark 來喚醒,或者提前被 unpark 發(fā)放了喚醒許可。

那有沒有一個(gè)方法,僅僅讓線程掛起,只能通過等待超時(shí)時(shí)間到了再被喚醒呢。

這個(gè)方法就是

Thread.sleep(long)

我們把它補(bǔ)充在圖里,這一部分就全了。

再把它加到全局圖中。


后記

Java 線程的狀態(tài),有六種

  1. NEW
  2. RUNNABLE
  3. BLOCKED
  4. WAITING
  5. TIMED_WAITING
  6. TERMINATED

而經(jīng)典的線程五態(tài)模型,有五種狀態(tài)

  1. 創(chuàng)建
  2. 就緒
  3. 執(zhí)行
  4. 阻塞
  5. 終止

不同實(shí)現(xiàn)者,可能有合并和拆分。

比如 Java 將五態(tài)模型中的就緒和執(zhí)行,都統(tǒng)一成 RUNNABLE,將阻塞(即不可能得到 CPU 運(yùn)行機(jī)會的狀態(tài))細(xì)分為了 BLOCKED、WAITING、TIMED_WAITING,這里我們不去評價(jià)好壞。

也就是說,BLOCKED、WAITING、TIMED_WAITING 這幾個(gè)狀態(tài),線程都不可能得到 CPU 的運(yùn)行權(quán),你叫它掛起、阻塞、睡眠、等待,都可以,很多文章,你也會看到這幾個(gè)詞沒那么較真地來回用。

再說兩個(gè)你可能困惑的問題。

調(diào)用 jdk 的 Lock 接口中的 lock,如果獲取不到鎖,線程將掛起,此時(shí)線程的狀態(tài)是什么呢?

有多少同學(xué)覺得應(yīng)該和 synchronized 獲取不到鎖的效果一樣,是變成 BLOCKED 狀態(tài)?

不過如果你仔細(xì)看我上面的文章,有一句話提到了,jdk 中鎖的實(shí)現(xiàn),是基于 AQS 的,而 AQS 的底層,是用 park 和 unpark 來掛起和喚醒線程,所以應(yīng)該是變?yōu)?WAITING 或 TIMED_WAITING 狀態(tài)。

調(diào)用阻塞 IO 方法,線程變成什么狀態(tài)?

比如 socket 編程時(shí),調(diào)用如 accept (),read () 這種阻塞方法時(shí),線程處于什么狀態(tài)呢?

答案是處于 RUNNABLE 狀態(tài),但實(shí)際上這個(gè)線程是得不到運(yùn)行權(quán)的,因?yàn)樵诓僮飨到y(tǒng)層面處于阻塞態(tài),需要等到 IO 就緒,才能變?yōu)榫途w態(tài)。

但是在 Java 層面,JVM 認(rèn)為等待 IO 與等待 CPU 執(zhí)行權(quán),都是一樣的,人家就是這么認(rèn)為的,這里我仍然不討論其好壞,你覺得這么認(rèn)為不爽,可以自己設(shè)計(jì)一門語言,那你想怎么認(rèn)為,別人也拿你沒辦法。

比如要我設(shè)計(jì)語言,我就認(rèn)為可被 CPU 調(diào)度執(zhí)行的線程,處于死亡態(tài)。這樣我的這門語言一定會有個(gè)經(jīng)典面試題,為什么閃客把可運(yùn)行的線程定義為死亡態(tài)呢?

OK,今天的文章就到這里。

本篇文章寫得有點(diǎn)投入,寫到這發(fā)現(xiàn)把開頭都小宇都給忘了。

責(zé)任編輯:龐桂玉 來源: IT之家
相關(guān)推薦

2022-03-23 08:51:21

線程池Java面試題

2019-02-25 17:42:43

TCP協(xié)議狀態(tài)轉(zhuǎn)換

2021-12-26 18:22:30

Java線程多線程

2021-12-28 09:10:55

Java線程狀態(tài)

2012-05-15 02:18:31

Java線程池

2024-11-28 11:07:50

線程JVM操作系統(tǒng)

2017-07-06 15:36:56

線程線程安全開發(fā)

2010-06-28 17:00:58

FTP傳輸模式

2015-08-13 10:48:39

Java 8轉(zhuǎn)換及改進(jìn)

2024-12-31 09:00:12

Java線程狀態(tài)

2013-12-09 09:56:30

NAT64IPv6stateful

2010-06-18 12:38:38

UML狀態(tài)機(jī)視圖

2023-11-29 16:29:09

線程java

2024-10-11 15:04:35

KafkaLeader選舉

2022-08-29 16:03:33

狀態(tài)流轉(zhuǎn)Java

2009-03-04 10:11:58

StringsjavaSun

2017-03-26 23:16:05

Java線程jstack

2017-06-09 15:17:48

Java線程jstack

2010-05-27 14:42:40

Linux查看端口

2022-07-29 07:48:15

HTTP常用狀態(tài)碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號