成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Java 線程的狀態及轉換

開發 后端
將弱化 threadStatus 這個整數值了,就直接說改變了其線程狀態,大家知道其實就只是改變了 threadStatus 的值而已。

低并發編程

戰略上藐視技術,戰術上重視技術

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

小宇:今天去面試了,面試官問我 Java 線程的狀態及其轉化。

閃客:哦哦,很常見的面試題呀,不是有一張狀態流轉圖嘛。

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


閃客:哈哈哈。

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

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

小宇:好滴。

線程狀態的實質

首先你得明白,當我們說一個線程的狀態時,說的是什么?

沒錯,就是一個變量的值而已。

哪個變量?

Thread 類中的一個變量,叫

private volatile int threadStatus = 0;

這個值是個整數,不方便理解,可以通過映射關系(VM.toThreadState),轉換成一個枚舉類。

public enum State {

NEW,

RUNNABLE,

BLOCKED,

WAITING,

TIMED_WAITING,

TERMINATED;

}

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

就是這么簡單。

NEW

現在我們還沒有任何 Thread 類的對象呢,也就不存在線程狀態一說。

一切的起點,要從把一個 Thread 類的對象創建出來,開始說起。

Thread t = new Thread();

當然,你后面可以接很多參數。

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

你也可以 new 一個繼承了 Thread 類的子類。

Thread t = new MyThread();

你說線程池怎么不 new 就可以有線程了呢?人家內部也是 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;
}
}
}

總是,一切的開始,都要調用 Thread 類的構造方法。

而這個構造方法,最終都會調用 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();
}

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

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

而這個值對應的狀態,就是 STATE.NEW,非要翻譯成中文,就叫初始態吧。

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

之后的分析,將弱化 threadStatus 這個整數值了,就直接說改變了其線程狀態,大家知道其實就只是改變了 threadStatus 的值而已。

RUNNABLE

你說,剛剛處于 NEW 狀態的線程,對應操作系統里的什么狀態呢?

一看你就沒仔細看我上面的分析。

Thread t = new Thread();

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

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

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

躺在堆內存中無所事事的 Thread 對象,在調用了 start () 方法后,才顯現生機。

t.start();

這個方法一調用,那可不得了,最終會調用到一個討厭的 native 方法里。

private native void start0();

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

九曲十八彎跟進 jvm 源碼之后,調用到了這個方法。

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

大名鼎鼎的 unix 創建線程的方法,pthread_create。

此時,在操作系統內核中,才有了一個真正的線程,被創建出來。

而 linux 操作系統,是沒有所謂的剛創建但沒啟動的線程這種說法的,創建即刻開始運行。

雖然無法從源碼發現線程狀態的變化,但通過 debug 的方式,我們看到調用了 Thread.start () 方法后,線程的狀態變成了 RUNNABLE,運行態。

那我們的狀態圖又豐富了起來。


通過這部分,我們知道如下幾點:

1. 在 Java 調用 start () 后,操作系統中才真正出現了一個線程,并且立刻運行。

2. Java 中的線程,和操作系統內核中的線程,是一對一的關系。

3. 調用 start 后,線程狀態變為 RUNNABLE,這是由 native 方法里的某部分代碼造成的。

RUNNING 和 READY

CPU 一個核心,同一時刻,只能運行一個線程。

具體執行哪個線程,要看操作系統 的調度機制。

所以,上面的 RUNNABLE 狀態,準確說是,得到了可以隨時準備運行的機會的狀態。

而處于這個狀態中的線程,也分為了正在 CPU 中運行的線程,和一堆處于就緒中等待 CPU 分配時間片來運行的線程。

處于就緒中的線程,會存儲在一個就緒隊列中,等待著被操作系統的調度機制選到,進入 CPU 中運行。

當然,要注意,這里的 RUNNING 和 READY 狀態,是我們自己為了方便描述而造出來的。

無論是 Java 語言,還是操作系統,都不區分這兩種狀態,在 Java 中統統叫 RUNNABLE。

TERMINATED

當一個線程執行完畢(或者調用已經不建議的 stop 方法),線程的狀態就變為 TERMINATED。

此時這個線程已經無法死灰復燃了,如果你此時再強行執行 start 方法,將會報出錯誤。

java.lang.IllegalThreadStateException

很簡單,因為 start 方法的第一行就是這么直戳了當地寫的。

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

誒,那如果此時強行把 threadStatus 改成 0,會怎么樣呢?你可以試試喲。

BLOCKED

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

初始 -- 運行 -- 終止

沒有發生任何的障礙。

接下來,就稍稍復雜一點了,我們讓線程碰到些障礙。

首先創建一個對象 lock。

public static final Object lock = new Object();

一個線程,執行一個 sychronized 塊,鎖對象是 lock,且一直持有這把鎖不放。

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

另一個線程,也同樣執行一個鎖對象為 lock 的 sychronized 塊。

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

那么,在進入 synchronized 塊時,因為無法拿到鎖,會使線程狀態變為 BLOCKED。

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

當該線程獲取到了鎖后,便可以進入 synchronized 塊,此時線程狀態變為 RUNNABLE。

因此我們得出如下轉換關系。

當然,這只是線程狀態的改變,線程還發生了一些實質性的變化。

我們不考慮虛擬機對 synchronized 的極致優化。

當進入 synchronized 塊或方法,獲取不到鎖時,線程會進入一個該鎖對象的同步隊列。

當持有鎖的這個線程,釋放了鎖之后,會喚醒該鎖對象同步隊列中的所有線程,這些線程會繼續嘗試搶鎖。如此往復。

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


線程 1 釋放鎖,線程 2、3、4 重新變為 RUNNABLE,繼續搶鎖,假如此時線程 3 搶到了鎖。

如此往復。

WAITING

這部分是最復雜的,同時也是面試中考點最多的,將分成三部分講解。聽我說完后你會發現,這三部分有很多相同但地方,不再是孤立的知識點。

wait/notify

我們在剛剛的 synchronized 塊中加點東西。

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

當這個 lock.wait () 方法一調用,會發生三件事。

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

2. 線程狀態變成 WAITING

3. 線程進入 lock 對象的等待隊列


什么時候這個線程被喚醒,從等待隊列中移出,并從 WAITING 狀態返回 RUNNABLE 狀態呢?

必須由另一個線程,調用同一個對象的 notify / notifyAll 方法。

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

只不過 notify 是只喚醒一個線程,而 notifyAll 是喚醒所有等待隊列中的線程。

但需要注意,被喚醒后的線程,從等待隊列移出,狀態變為 RUNNABLE,但仍然需要搶鎖,搶鎖成功了,才可以從 wait 方法返回,繼續執行。

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


所以我們的整個流程圖,現在變成了這個樣子。


join

主線程這樣寫。

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

當執行到 t.join () 的時候,主線程會變成 WAITING 狀態,直到線程 t 執行完畢,主線程才會變回 RUNNABLE 狀態,繼續往下執行。

看起來,就像是主線程執行過程中,另一個線程插隊加入(join),而且要等到其結束后主線程才繼續。

因此我們的狀態圖,又多了兩項。

那 join 又是怎么神奇地實現這一切呢?也是像 wait 一樣放到等待隊列么?

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

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

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

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

那從 WAITING 回到 RUNNABLE 是怎么實現的呢?

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

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

沒錯,就是這樣。

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

果然,找到了如下代碼。

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

我們看到,虛擬機在一個線程的方法執行完畢后,執行了個 ensure_join 方法,看名字就知道是專門為 join 而設計的。

而繼續跟進會發現一段關鍵代碼,lock.notify_all,這便是一個線程結束后,會自動調用自己的 notifyAll 方法的證明。

所以,其實 join 就是 wait,線程結束就是 notifyAll。現在,是不是更清晰了。


park/unpark

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

一個線程調用如下方法。

LockSupport.park()

該線程狀態會從 RUNNABLE 變成 WAITING、

另一個線程調用

LockSupport.unpark (Thread 剛剛的線程)

剛剛的線程會從 WAITING 回到 RUNNABLE

但從線程狀態流轉來看,與 wait 和 notify 相同。

從實現機制上看,他們甚至更為簡單。

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

2. 沒有什么等待隊列一說,unpark 會精準喚醒某一個確定的線程。

3. park 和 unpark 沒有順序要求,可以先調用 unpark

關于第三點,就涉及到 park 的原理了,這里我只簡單說明。

線程有一個計數器,初始值為 0

調用 park 就是

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

調用 unpark 就是

將這個值改為 1

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

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

park 的使用非常簡單,同時也是 JDK 中鎖實現的底層。它的 JVM 及操作系統層面的原理很復雜,改天可以專門找一節來講解。

現在我們的狀態圖,又可以更新了。


TIMED_WAITING

這部分就再簡單不過了,將上面導致線程變成 WAITING 狀態的那些方法,都增加一個超時參數,就變成了將線程變成 TIMED_WAITING 狀態的方法了,我們直接更新流程圖。

這些方法的唯一區別就是,從 TIMED_WAITING 返回 RUNNABLE,不但可以通過之前的方式,還可以通過到了超時時間,返回 RUNNABLE 狀態。

就這樣。

還有,大家看。

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

join 就是 wait 的封裝。

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

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

這個方法就是

Thread.sleep(long)

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

再把它加到全局圖中。


后記

Java 線程的狀態,有六種

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

而經典的線程五態模型,有五種狀態

  1. 創建
  2. 就緒
  3. 執行
  4. 阻塞
  5. 終止

不同實現者,可能有合并和拆分。

比如 Java 將五態模型中的就緒和執行,都統一成 RUNNABLE,將阻塞(即不可能得到 CPU 運行機會的狀態)細分為了 BLOCKED、WAITING、TIMED_WAITING,這里我們不去評價好壞。

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

再說兩個你可能困惑的問題。

調用 jdk 的 Lock 接口中的 lock,如果獲取不到鎖,線程將掛起,此時線程的狀態是什么呢?

有多少同學覺得應該和 synchronized 獲取不到鎖的效果一樣,是變成 BLOCKED 狀態?

不過如果你仔細看我上面的文章,有一句話提到了,jdk 中鎖的實現,是基于 AQS 的,而 AQS 的底層,是用 park 和 unpark 來掛起和喚醒線程,所以應該是變為 WAITING 或 TIMED_WAITING 狀態。

調用阻塞 IO 方法,線程變成什么狀態?

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

答案是處于 RUNNABLE 狀態,但實際上這個線程是得不到運行權的,因為在操作系統層面處于阻塞態,需要等到 IO 就緒,才能變為就緒態。

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

比如要我設計語言,我就認為可被 CPU 調度執行的線程,處于死亡態。這樣我的這門語言一定會有個經典面試題,為什么閃客把可運行的線程定義為死亡態呢?

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

本篇文章寫得有點投入,寫到這發現把開頭都小宇都給忘了。

責任編輯:龐桂玉 來源: IT之家
相關推薦

2022-03-23 08:51:21

線程池Java面試題

2019-02-25 17:42:43

TCP協議狀態轉換

2021-12-26 18:22:30

Java線程多線程

2021-12-28 09:10:55

Java線程狀態

2012-05-15 02:18:31

Java線程池

2024-11-28 11:07:50

線程JVM操作系統

2017-07-06 15:36:56

線程線程安全開發

2010-06-28 17:00:58

FTP傳輸模式

2015-08-13 10:48:39

Java 8轉換及改進

2024-12-31 09:00:12

Java線程狀態

2013-12-09 09:56:30

NAT64IPv6stateful

2010-06-18 12:38:38

UML狀態機視圖

2023-11-29 16:29:09

線程java

2024-10-11 15:04:35

KafkaLeader選舉

2022-08-29 16:03:33

狀態流轉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常用狀態碼
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久99精品国产99久久6男男 | 日韩一区二区三区视频在线观看 | 一区二区三区精品视频 | 99热精品国产 | 久久y| 欧美理论片在线观看 | 亚洲精品电影网在线观看 | 久久免费视频在线 | 国产专区在线 | 国产精品免费一区二区三区 | 成人久久18免费网站麻豆 | 久久精品国产免费 | 久久精品国产免费 | 国产精品欧美精品日韩精品 | 黑人巨大精品欧美一区二区免费 | 中文字字幕在线中文乱码范文 | 成人免费网站 | 自拍在线 | 99久久免费精品视频 | 久久久久国产一区二区三区四区 | 精品成人一区 | 精品91久久 | 久久精品亚洲精品国产欧美 | 欧美日韩亚洲在线 | 综合五月婷 | 一级片视频免费观看 | 国产精品成人一区二区 | 免费视频一区二区 | 国产成人aⅴ | 成人在线视频免费观看 | 成人一区二区三区在线观看 | av看看| 久久久www成人免费无遮挡大片 | 欧美成人手机视频 | 天天看天天爽 | 国产精品日韩 | 一级黄色毛片 | 久久一区二区三区四区五区 | 日韩淫片免费看 | 亚洲一区二区中文字幕 | jav成人av免费播放 |