Java高併發學習筆記(一):Thread詳解

1 來源

  • 來源:《Java高併發編程詳解 多線程與架構設計》,汪文君著
  • 章節:第1、2、三章

本文是前三章的筆記整理。java

2 概述

本文主要講述了線程的生命週期、Thread類的構造方法以及經常使用API,最後介紹了線程的關閉方法。編程

3 線程生命週期

3.1 五個階段

線程生命週期能夠分爲五個階段:bash

  • NEW
  • RUNNABLE
  • RUNNING
  • BLOCKED
  • TERMINATED

3.2 NEW

new建立一個Thread對象時,可是並無使用start()啓動線程,此時線程處於NEW狀態。準確地說,只是Thread對象的狀態,這就是一個普通的Java對象。此時能夠經過start()方法進入RUNNABLE狀態。網絡

3.3 RUNNABLE

進入RUNNABLE狀態必須調用start()方法,這樣就在JVM中建立了一個線程。可是,線程一經建立,並不能立刻被執行,線程執行與否須要聽令於CPU調度,也就是說,此時是處於可執行狀態,具有執行的資格,可是並無真正執行起來,而是在等待被調度。多線程

RUNNABLE狀態只能意外終止或進入RUNNING狀態。架構

3.4 RUNNING

一旦CPU經過輪詢或其餘方式從任務可執行隊列中選中了線程,此時線程才能被執行,也就是處於RUNNING狀態,在該狀態中,可能發生的狀態轉換以下:併發

  • 進入TERMINATED:好比調用已經不推薦的stop()方法
  • 進入BLOCKED:好比調用了sleep()/wait()方法,或者進行某個阻塞操做(獲取鎖資源、磁盤IO等)
  • 進入RUNNABLECPU時間片到,或者線程主動調用yield()

3.5 BLOCKED

也就是阻塞狀態,進入阻塞狀態的緣由不少,常見的以下:ide

  • 磁盤IO
  • 網絡操做
  • 爲了獲取鎖而進入阻塞操做

處於BLOCKED狀態時,可能發生的狀態轉換以下:高併發

  • 進入TERMINATED:好比調用不推薦的stop(),或者JVM意外死亡
  • 進入RUNNABLE:好比休眠結束、被notify()/nofityAll()喚醒、獲取到某個鎖、阻塞過程被interrupt()打斷等

3.6 TERMINATED

TERMINATED是線程的最終狀態,進入該狀態後,意味着線程的生命週期結束,好比在下列狀況下會進入該狀態:工具

  • 線程運行正常結束
  • 線程運行出錯意外結束
  • JVM意外崩潰,致使全部線程都強制結束

4 Thread構造方法

4.1 構造方法

Thread的構造方法一共有八個,這裏根據命名方式分類,使用默認命名的構造方法以下:

  • Thread()
  • Thread(Runnable target)
  • Thread(ThreadGroup group,Runnable target)

命名線程的構造方法以下:

  • Thread(String name)
  • Thread(Runnable target,Strintg name)
  • Thread(ThreadGroup group,String name)
  • Thread(ThreadGroup group,Runnable target,String name)
  • Thread(ThreadGroup group,Runnable target,String name,long stackSize)

但實際上全部的構造方法最終都是調用以下私有構造方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);

在默認命名構造方法中,在源碼中能夠看到,默認命名其實就是Thread-X的命令(X爲數字):

public Thread() {
    this((ThreadGroup)null, (Runnable)null, "Thread-" + nextThreadNum(), 0L);
}

public Thread(Runnable target) {
    this((ThreadGroup)null, target, "Thread-" + nextThreadNum(), 0L);
}

private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

而在命名構造方法就是自定義的名字。

另外,若是想修改線程的名字,能夠調用setName()方法,可是須要注意,處於NEW狀態的線程才能修改。

4.2 線程的父子關係

Thread的全部構造方法都會調用以下方法:

private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals);

其中的一段源碼截取以下:

if (name == null) {
    throw new NullPointerException("name cannot be null");
} else {
    this.name = name;
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        if (security != null) {
            g = security.getThreadGroup();
        }

        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
}

能夠看到當前這裏有一個局部變量叫parent,而且賦值爲currentThread()currentThread()是一個native方法。由於一個線程被建立時的最初狀態爲NEW,所以currentThread()表明是建立自身線程的那個線程,也就是說,結論以下:

  • 一個線程的建立確定是由另外一個線程完成的
  • 被建立線程的父線程是建立它的線程

也就是本身建立的線程,父線程爲main線程,而main線程由JVM建立。

另外,Thread的構造方法中有幾個具備ThreadGroup參數,該參數指定了線程位於哪個ThreadGroup,若是一個線程建立的時候沒有指定ThreadGroup,那麼將會和父線程同一個ThreadGroupmain線程所在的ThreadGroup稱爲main

4.3 關於stackSize

Thread構造方法中有一個stackSize參數,該參數指定了JVM分配線程棧的地址空間的字節數,對平臺依賴性較高,在一些平臺上:

  • 設置較大的值:可使得線程內調用遞歸深度增長,下降StackOverflowError出現的機率
  • 設置較低的值:可使得建立的線程數增多,能夠推遲OutOfMemoryError出現的時間

可是,在一些平臺上該參數不會起任何做用。另外,若是設置爲0也不會起到任何做用。

5 Thread API

5.1 sleep()

sleep()有兩個重載方法:

  • sleep(long mills)
  • sleep(long mills,int nanos)

可是在JDK1.5後,引入了TimeUnit,其中對sleep()方法提供了很好的封裝,建議使用TimeUnit.XXXX.sleep()去代替Thread.sleep()

TimeUnit.SECONDS.sleep(1);
TimeUnit.MINUTES.sleep(3);

5.2 yield()

yield()屬於一種啓發式方法,提醒CPU調度器當前線程會自願放棄資源,若是CPU資源不緊張,會忽略這種提醒。調用yield()方法會使當前線程從RUNNING變爲RUNNABLE狀態。

關於yield()sleep()的區別,區別以下:

  • sleep()會致使當前線程暫停指定的時間,沒有CPU時間片的消耗
  • yield()只是對CPU調度器的一個提示,若是CPU調度器沒有忽略這個提示,會致使線程上下文的切換
  • sleep()會使線程短暫阻塞,在給定時間內釋放CPU資源
  • 若是yield()生效,yield()會使得從RUNNING狀態進入RUNNABLE狀態
  • sleep()會幾乎百分百地完成給定時間的休眠,可是yield()的提示不必定能擔保
  • 一個線程調用sleep()而另外一個線程調用interrupt()會捕獲到中斷信號,而yield則不會

5.3 setPriority()

5.3.1 優先級介紹

線程與進程相似,也有本身的優先級,理論上來講,優先級越高的線程會有優先被調度的機會,但實際上並非如此,設置優先級與yield()相似,也是一個提醒性質的操做:

  • 對於root用戶,會提醒操做系統想要設置的優先級別,不然會被忽略
  • 若是CPU比較忙,設置優先級可能會得到更多的CPU時間片,可是空閒時優先級的高低幾乎不會有任何做用

因此,設置優先級只是很大程度上讓某個線程儘量得到比較多的執行機會,也就是讓線程本身儘量被操做系統調度,而不是設置了高優先級就必定優先運行,或者說優先級高的線程比優先級低的線程就必定優先運行。

5.3.2 優先級源碼分析

設置優先級直接調用setPriority()便可,OpenJDK 11源碼以下:

public final void setPriority(int newPriority) {
    this.checkAccess();
    if (newPriority <= 10 && newPriority >= 1) {
        ThreadGroup g;
        if ((g = this.getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }

            this.setPriority0(this.priority = newPriority);
        }

    } else {
        throw new IllegalArgumentException();
    }
}

能夠看到優先級處於[1,10]之間,並且不能設置爲大於當前ThreadGroup的優先級,最後經過native方法setPriority0設置優先級。

通常狀況下,不會對線程的優先級設置級別,默認狀況下,線程的優先級爲5,由於main線程的優先級爲5,並且main爲全部線程的父進程,所以默認狀況下線程的優先級也是5。

5.4 interrupt()

interrupt()是一個重要的API,線程中斷的API有以下三個:

  • void interrupt()
  • boolean isInterrupted()
  • static boolean interrupted()

下面對其逐一進行分析。

5.4.1 interrupt()

一些方法調用會使得當前線程進入阻塞狀態,好比:

  • Object.wait()
  • Thread.sleep()
  • Thread.join()
  • Selector.wakeup()

而調用interrupt()能夠打斷阻塞,打斷阻塞並不等於線程的生命週期結束,僅僅是打斷了當前線程的阻塞狀態。一旦在阻塞狀態下被打斷,就會拋出一個InterruptedException的異常,這個異常就像一個信號同樣通知當前線程被打斷了,例子以下:

public static void main(String[] args) throws InterruptedException{
    Thread thread = new Thread(()->{
        try{
            TimeUnit.SECONDS.sleep(10);
        }catch (InterruptedException e){
            System.out.println("Thread is interrupted.");
        }
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    thread.interrupt();
}

會輸出線程被中斷的信息。

5.4.2 isInterrupted()

isInterrupted()能夠判斷當前線程是否被中斷,僅僅是對interrupt()標識的一個判斷,並不會影響標識發生任何改變(由於調用interrupt()的時候會設置內部的一個叫interrupt flag的標識),例子以下:

public static void main(String[] args) throws InterruptedException{
    Thread thread = new Thread(()->{
        while (true){}
    });
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :"+thread.isInterrupted());
    thread.interrupt();
    System.out.println("Thread is interrupted :"+thread.isInterrupted());
}

輸出結果爲:

Thread is interrupted :false
Thread is interrupted :true

另外一個例子以下:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    System.out.println("In catch block thread is interrupted :" + isInterrupted());
                }
            }
        }
    };
    thread.start();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :" + thread.isInterrupted());
    thread.interrupt();
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Thread is interrupted :" + thread.isInterrupted());
}

輸出結果:

Thread is interrupted :false
In catch block thread is interrupted :false
Thread is interrupted :false

一開始線程未被中斷,結果爲false,調用中斷方法後,在循環體內捕獲到了異常(信號),此時會Thread自身會擦除interrupt標識,將標識復位,所以捕獲到異常後輸出結果也爲false

5.4.3 interrupted()

這是一個靜態方法,調用該方法會擦除掉線程的interrupt標識,須要注意的是若是當前線程被打斷了:

  • 第一次調用interrupted()會返回true,而且當即擦除掉interrupt標識
  • 第二次包括之後的調用永遠都會返回false,除非在此期間線程又一次被打斷

例子以下:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.interrupted());
            }
        }
    };
    thread.setDaemon(true);
    thread.start();
    TimeUnit.MILLISECONDS.sleep(2);
    thread.interrupt();
}

輸出(截取一部分):

false
false
false
true
false
false
false

能夠看到其中帶有一個true,也就是interrupted()判斷到了其被中斷,此時會當即擦除中斷標識,而且只有該次返回true,後面都是false

關於interrupted()isInterrupted()的區別,能夠從源碼(OpenJDK 11)知道:

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}

public boolean isInterrupted() {
    return this.isInterrupted(false);
}

@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean var1);

實際上二者都是調用同一個native方法,其中的布爾變量表示是否擦除線程的interrupt標識:

  • true表示想要擦除,interrupted()就是這樣作的
  • false表示不想擦除,isInterrupted()就是這樣作的

5.5 join()

5.5.1 join()簡介

join()sleep()同樣,都是屬於能夠中斷的方法,若是其餘線程執行了對當前線程的interrupt操做,也會捕獲到中斷信號,而且擦除線程的interrupt標識,join()提供了三個API,分別以下:

  • void join()
  • void join(long millis,int nanos)
  • void join(long mills)

5.5.2 例子

一個簡單的例子以下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = IntStream.range(1,3).mapToObj(Main::create).collect(Collectors.toList());
        threads.forEach(Thread::start);
        for (Thread thread:threads){
            thread.join();
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" # "+i);
            shortSleep();
        }
    }

    private static Thread create(int seq){
        return new Thread(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+" # "+i);
                shortSleep();
            }
        },String.valueOf(seq));
    }

    private static void shortSleep(){
        try{
            TimeUnit.MILLISECONDS.sleep(2);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

輸出截取以下:

2 # 8
1 # 8
2 # 9
1 # 9
main # 0
main # 1
main # 2
main # 3
main # 4

線程1和線程2交替執行,而main線程會等到線程1和線程2執行完畢後再執行。

6 線程關閉

Thread中有一個過期的方法stop,能夠用於關閉線程,可是存在的問題是有可能不會釋放monitor的鎖,所以不建議使用該方法關閉線程。線程的關閉能夠分爲三類:

  • 正常關閉
  • 異常退出
  • 假死

6.1 正常關閉

6.1.1 正常結束

線程運行結束後,就會正常退出,這是最普通的一種狀況。

6.1.2 捕獲信號關閉線程

經過捕獲中斷信號去關閉線程,例子以下:

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(){
        @Override
        public void run() {
            System.out.println("work...");
            while(!isInterrupted()){

            }
            System.out.println("exit...");
        }
    };
    t.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("System will be shutdown.");
    t.interrupt();
}

一直檢查interrupt標識是否設置爲true,設置爲true則跳出循環。另外一種方式是使用sleep()

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(){
        @Override
        public void run() {
            System.out.println("work...");
            while(true){
                try{
                    TimeUnit.MILLISECONDS.sleep(1);
                }catch (InterruptedException e){
                    break;
                }
            }
            System.out.println("exit...");
        }
    };
    t.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("System will be shutdown.");
    t.interrupt();
}

6.1.3 volatile

因爲interrupt標識頗有可能被擦除,或者不會調用interrupt()方法,所以另外一種方法是使用volatile修飾一個布爾變量,並不斷循環判斷:

public class Main {
    static class MyTask extends Thread{
        private volatile boolean closed = false;

        @Override
        public void run() {
            System.out.println("work...");
            while (!closed && !isInterrupted()){

            }
            System.out.println("exit...");
        }

        public void close(){
            this.closed = true;
            this.interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyTask t = new MyTask();
        t.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("System will be shutdown.");
        t.close();
    }
}

6.2 異常退出

線程執行單元中是不容許拋出checked異常的,若是在線程運行過程當中須要捕獲checked異常而且判斷是否還有運行下去的必要,能夠將checked異常封裝爲unchecked異常,好比RuntimeException,拋出從而結束線程的生命週期。

6.3 假死

所謂假死就是雖然線程存在,可是卻沒有任何的外在表現,好比:

  • 沒有日誌輸出
  • 不進行任何的做業

等等,雖然此時線程是存在的,但看起來跟死了同樣,事實上是沒有死的,出現這種狀況,很大多是由於線程出現了阻塞,或者兩個線程爭奪資源出現了死鎖。

這種狀況須要藉助一些外部工具去判斷,好比VisualVMjconsole等等,找出存在問題的線程以及當前的狀態,並判斷是哪一個方法形成了阻塞。

相關文章
相關標籤/搜索