天天看點

一個線程調用兩次 start()方法會出現什麼情況?

一個線程兩次調用 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)狀态,但是不論如何,都是不可以再次啟動的。
一個線程調用兩次 start()方法會出現什麼情況?

線程狀态轉換圖

線程是什麼?

從作業系統的角度,可以簡單認為,線程是系統排程的最小單元,一個程序可以包含多個線程,作為任務的真正運作者,有自己的棧( 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()      
  1. 優點

    這種直接調用 JNI 形式的本地代碼能精細的控制線程和相關的并發操作,并且擁有高擴充能力。

  2. 缺點

    實作複雜,提高了并發程式設計的門檻。

線程的基本操作

建立線程

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.

一個線程調用兩次 start()方法會出現什麼情況?

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();// 清理廢棄條目,如果超限,則擴容
        }      

微信号:程式員開發者社群

部落格:王小明