一個線程兩次調用 start 會出現什麼情況?
一個線程兩次調用 start()方法會出現什麼情況?談談線程的生命周期和狀态轉移。在第二次調用 start() 方法的時候,線程可能處于終止或者其他(非NEW)狀态,但是不論如何,都是不可以再次啟動的。
調用兩次 start ?
Java的線程是不允許啟動兩次的,第二次調用必然會抛岀 IllegalThreadStateEXception,這是一種運作時異常,多次調用 start 被認為是程式設計錯誤。
線程的生命周期
關于線程生命周期的不同狀态,在Java5以後,線程狀态被明确定義在其公共内部枚舉類型java.ang. Thread. State中,分别是:
- 建立(NEW),表示線程被建立出來還沒真正啟動的狀态,可以認為它是個Java内部狀态。
- 就緒( RUNNABLE),表示該線程已經在wM中執行,當然由于執行需要計算資源,它可能是正在運作,也可能還在等待系統配置設定給它CP∪片段,在就緒隊列裡面排隊。
- 運作(Running)在其他一些分析中,會額外區分一種狀态 RUNNING,但是從 Java aPi的角度,并不能表示出來。
- 阻塞( BLOCKED),這個狀态和我們前面兩講介紹的同步非常相關,阻塞表示線程在等待 Monitor lock。比如,線程試圖通過synchronized去擷取某個鎖,但是其他線程已經獨占了,那麼目前線程就會處于阻塞狀态。
- 等待( WAITING),表示正在等待其他線程釆取某些操作。一個常見的場景是類似生産者消費者模式,發現任務條件尚未滿足,就讓目前消費者線程等待(wait),另外的生産者線程去準備任務資料,然後通過類似 notify等動作,通知消費線程可以繼續工作了。Thread join(也會令線程進入等待狀态。
- 計時等待( TIMED_WAIT),其進入條件和等待狀态類似,但是調用的是存在逾時條件的方法,比如wait或join等方法的指定逾時版本,如下面示例
public final native void wait(long timeout) throws InterruptedException;
- 終止( TERMINATED),不管是意外退出還是正常執行結束,線程已經完成使命,終止運作,也有人把這個狀态叫作死亡在第二次調用 start()方法的時候,線程可能處于終止或者其他(非NEW)狀态,但是不論如何,都是不可以再次啟動的。
線程狀态轉換圖
線程是什麼?
從作業系統的角度,可以簡單認為,線程是系統排程的最小單元,一個程序可以包含多個線程,作為任務的真正運作者,有自己的棧( Stack)、寄存器( Register)、本地存儲
( Thread Local)等,但是會和迸程内其他線程共享檔案描述符、虛拟位址空間等
線程的分類
在具體實作中,線程還分為核心線程、使用者線程,Java的線程實作其實是與虛拟機相關的。對于我們最熟悉的sun/ Oracle jDK,其線程也經曆了一個演進過程,基本上在Java1.2之後,JDK已經抛棄了所謂的 Green Thread,也就是使用者排程的線程,現在的模型是一對一映射到作業系統核心線程。
- https://en.wikipedia.org/wiki/Green_threads
Thread 源碼
Thread 源碼中大部分邏輯是直接調用 JNI 本地代碼。
private native void start0();
private native void setPriority0 (int newPriority);
private native void interrupt0()
-
優點
這種直接調用 JNI 形式的本地代碼能精細的控制線程和相關的并發操作,并且擁有高擴充能力。
-
缺點
實作複雜,提高了并發程式設計的門檻。
線程的基本操作
建立線程
Runnable task =()->System.out.println("Hello World!");
Thread myThread = new Thread(task);
myThread.start();
myThread.join();
直接擴充 Thread 類,然後執行個體化,上面的例子中, 實作一個 Runnable,将代碼邏輯放在 Runnable 中,然後使用 Thread 并啟動 start ,等待 join 結束。Runnable 的好處是,不會有多繼承的限制,重用代碼實作,可以實作重複邏輯。并且能夠更好的結合 Java 并發庫中的 Executor 架構使用。比如将上面的start,join 可以改寫成下面的代碼:
Runnable task =()->System.out.println("Hello World!");
Future future = (Future) Executors.newFixedThreadPool(1).submit(task).get();
使用上面的方式,可以不用操心線程的建立和管理。
哪些因素可能影響線程的狀态
線程自身的方法
除了 start 之外,還有多個 join 方法等待線程結束。yield 告訴排程器,主動讓出 CPU, 另外, 還有些過時方法 resume, stop, suspend,destroy ,stop。
基類 Object 中提供一些基礎的 wait/notify/notifyAll方法。
如果我們持有某個對象的某個 Monitor鎖,調用 wait 會讓目前線程處于等待狀态。直到其他線程 notify 或者 notifyAll。本質上是提供了 Monitor 的釋放和擷取能力。
并發類庫中的工具
比如 CountDownLatch.await() 會讓線程進入等待狀态,知道 latch 基數為0 ,就可以看做是線程間的通訊 Signal.
image
Thread 和 Object 方法 進行線程之間的排程和維護,比較晦澀難懂,一般使用 Java 并發包進行線程的使用。
守護線程
守護線程(Daemon Thread),需要一個長期駐留服務的程式,但是不希望其影響應用退出,就可以設定成守護線程。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true)
daemonThread.start()
再來看看 Spurious wakeuρ。尤其是在多核CP∪的系統中,線程等待存在一種可能,就是在沒有仼何線程廣播或者發岀信号的情況下,線程就被喚醒,如果處理不當就可能岀現詭異的并發問題,是以我們在等待條件過程中,建議采用下面模式來書寫。
- https://en.wikipedia.org/wiki/Spurious_wakeup
//推薦
while(isConditiono())){
waitForAConfition()
}
//不推薦,可能引入bug
if(isCondition()){
waitForAConfition();
}
ThreadLocal
ThreadLocal 内部條目是弱引用,
ThreadLocalMap裡面的資料存儲結構,從上面的代碼來看,ThreadLocalMap中存放的就是Entry,Entry的KEY就是ThreadLocal,VALUE就是值。在ThreadLocal的get,set的時候都會清除線程Map裡所有key為null的value。但是如果不調用 get set 的話,value不會被清理,就存在記憶體洩露.
static class ThreadLocalMap {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
// 替換廢棄條目
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 掃碼并清理發現的廢棄條目,并檢查容量是否超限
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();// 清理廢棄條目,如果超限,則擴容
}
微信号:程式員開發者社群
部落格:王小明