天天看點

Java并發---①線程管理一.線程管理

一.線程管理

1.1 線程的建立方式

  • 繼承Thread類 (Thread類實作了Runnable接口)
public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("使用繼承的方式實作一個線程");
    }
}
           
  • 實作Runnable接口
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("使用實作Runnable接口的方式來實作一個線程");
    }
}
           

1.2 線程的運作

  • 如果使用的是繼承Thread類的方式,直接調用start()方法即可運作一個線程
  • 如果是使用實作Runnable接口的方式,那麼需要建立一個Thread,将自身作為參數傳入,然後調用建立的Thread對象的start()方法才可以運作。
public class Main {
    public static void main(String[] args){
        //第一種方式
        MyThread myThread = new MyThread();
        myThread.start();
        //第二種方式
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
           

調用線程的start方法後,将會建立一個新的線程,并執行線程中的run方法

如果直接調用該對象的run方法,那麼不會建立一個新的線程,而是會被主線程當成一個普通方法來執行。

1.3 線程資訊的擷取和設定

Thread類有一些儲存資訊的屬性,這些屬性可以用來辨別線程,顯示線程的狀态或者控制線程的優先級。

  • ID: 儲存了線程的唯一辨別符。
  • Name:儲存了線程的名字。
  • Priority:儲存了線程對象的優先級。線程的優先級從1到10,其中1是最低優先級,10是最高優先級。
  • Status:儲存了線程的狀态。在Java中,線程的狀态有6種:new,runnable,blocked,waiting,time waiting 或者 terminated。
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("***************************");
        System.out.println("id = " + Thread.currentThread().getId());
        System.out.println("name = " + Thread.currentThread().getName());
        System.out.println("priority = " + Thread.currentThread().getPriority());
        System.out.println("state = " + Thread.currentThread().getState());
        System.out.println("***************************");
    }
}
           
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        //設定線程名
        thread.setName("線程名字");
        //設定線程優先級
        thread.setPriority(10);
        thread.start();
    }
}
           

輸出資訊如下:

***************************
id = 14
name = 線程名字
priority = 10
state = RUNNABLE
***************************
           

1.4 線程的中斷

如果一個Java程式存在不止一個線程在執行,那麼必須要等到所有的線程都執行結束後, 這個Java程式才能運作結束。更準确的說,要等到所有的非守護線程運作結束後(守護線程的概念後面會介紹),或者其中某一個線程執行了System.exit()方法時,這個Java程式才運作結束。

如果一個線程執行時間過于久,那麼我們有沒有什麼辦法讓它停止呢?當然是存在的,Java為我們提供了中斷機制。這個機制要求線程檢測它是否被中斷了,然後再決定是否響應這個中斷請求。線程允許忽略中斷請求并且繼續執行。

public class MyThread extends Thread {
    @Override
    public void run(){
        int result = 0;
        while (true){
            System.out.println("result = " + result ++);
            if (this.isInterrupted()){
                System.out.println("線程已經被中斷");
                return;
            }
        }
    }
}
           
public class Main {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        //啟動線程
        myThread.start();
        try {
            //讓主線程休息一秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中斷線程
        myThread.interrupt();
    }
}
           

通過上面這個例子我們可以發行,當我們調用這個線程的interrupt方法時,線程并不會被強制中斷。這個方法隻會給這個線程打上一個标記,而是否中斷,則由我們在 run方法中的代碼所決定。

  • isInterrupted 方法:擷取線程的interrupted屬性的值 (推薦使用此方法)
  • interrupted 方法:靜态方法,擷取線程interrupted屬性的值,并将值置為false

1.5 線程中斷的控制

我們已經知道了調用線程的interrupt方法,并不能真正的讓線程中斷,那麼我們如何才能控制線程的中斷呢?

  • 方法一:stop方法

stop方法能讓運作中的線程強制停止下來,但是該方法已經被廢棄了,因為強行停止一個線程,會讓很多清理工作無法進行,也會讓線程獲得的鎖立刻釋放,進而帶來資料不一緻等等問題。

  • 方法二:interrupt + return

在外面調用線程的interrupt方法,線上程内部進行判斷,當線程的interrupted屬性為true時,直接return停止該線程。該方法線上程run方法的邏輯極為簡單時,可以使用。但是如果線程實作了複雜的算法并分布在幾個方法中,或者線程中有遞歸調用的方法,我們就得提供一個更好的機制來控制線程的中斷。

  • 方法三:interrupt + InterruptedException

下面這個示例是實作了在一個目錄下,尋找某個檔案的路徑,裡面涉及到遞歸調用。但是不管方法遞歸到了那一層,隻要線程被中斷了,就會抛出InterruptedException異常,并被run方法捕獲到,進而進行其他的工作。

public class FileSearch implements Runnable {

    private String initPath;
    private String fileName;

    public FileSearch(String initPath, String fileName) {
        this.initPath = initPath;
        this.fileName = fileName;
    }

    @Override
    public void run() {
        File file = new File(initPath);
        if (file.isDirectory()){
            try {
                directoryProcess(file);
            }catch (InterruptedException e){
                System.out.printf("%s: The search has been interrupted"
                        , Thread.currentThread().getName());
            }
        }
    }

    private void directoryProcess(File file) throws InterruptedException {
        File[] listFiles = file.listFiles();
        if (listFiles != null){
            for(File sonFile : listFiles){
                if (sonFile.isDirectory()){
                    directoryProcess(sonFile);
                }else {
                    fileProcess(sonFile);
                }
            }
        }
        if (Thread.interrupted()){
            throw new InterruptedException();
        }
    }

    private void fileProcess(File file) throws InterruptedException {
        if (file.getName().equals(fileName)){
            System.out.printf("%s : %s\n", Thread.currentThread().getName(), file.getAbsolutePath());
        }
        if (Thread.interrupted()){
            throw new InterruptedException();
        }
    }
           
public class Main {
    public static void main(String[] args){
        FileSearch fileSearch = new FileSearch("C://", "test.txt");
        Thread thread = new Thread(fileSearch);
        thread.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //中斷線程
        thread.interrupt();
    }
}
           

1.6 線程的休眠與恢複

有些時候,我們需要在某一個預期時間讓線程進行休眠,以釋放資源給其他線程使用。例如,程式的一個線程每隔一分鐘就會檢查一下傳感器的狀态,其餘時間不做任何操作。在這段空間時間,我們希望線程可以不占計算機的任何資源,當它繼續執行的直接到來時,JVM會選中它讓它繼續執行。我們可以通過**sleep()**方法來實作這個需求。

  • sleep方法接受整型數值作為參數,以表明線程挂起執行的毫秒數。
  • 通過TimeUnit枚舉元素也存在一個sleep方法,這個方法也是使用Thread類的sleep方法,但是它接受的機關由選擇的枚舉類型而定。
public class Main {
    public static void main(String[] args){
        System.out.println("before sleep : " + System.currentTimeMillis());
        try {
            //休眠一秒
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after sleep : " + System.currentTimeMillis());
        System.out.println("before sleep : " + System.currentTimeMillis());
        try {
            //休眠一分鐘
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after sleep : " + System.currentTimeMillis());
    }
}
           

大家可能發現了,每次執行sleep方法,都要顯示的捕獲一個InterruptedException異常,這是為什麼呢?與下面兩種場景有關

  • 處于休眠狀态的線程,如果調用了interrupt方法,該線程會立馬從休眠狀态喚醒,并抛出InterruptException異常,并且将線程的interrupted屬性置為false
  • 被标記了中斷的線程,如果調用了sleep方法,同樣也會抛出InterruptException異常,并且将線程的interrupted屬性置為false
public class MyThread1 extends Thread{
    @Override
    public void run(){
        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " interrupted : " + this.isInterrupted());
            System.out.println(Thread.currentThread().getName() + " 處于休眠中的線程被調用了 interrupt方法");
            System.out.println("******************");
            return;
        }
    }
}
           
public class MyThread2 extends Thread {
    @Override
    public void run(){
        while (true){
            try {
                if (this.isInterrupted()){
                    System.out.println(Thread.currentThread().getName() + "線程已經被中斷");
                    Thread.sleep(1000);
                }
            }catch (InterruptedException e){
                System.out.println(Thread.currentThread().getName() + " interrupted : " + this.isInterrupted());
                System.out.println(Thread.currentThread().getName() + " 中斷的線程調用了 sleep方法");
                return;
            }

        }
    }
}
           
public class Main {
    public static void main(String[] args){
        MyThread1 myThread1 = new MyThread1();
        myThread1.start();
        myThread1.interrupt();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        MyThread2 myThread2 = new MyThread2();
        myThread2.start();
        myThread2.interrupt();
    }
}

/*
結果如下:
Thread-0 interrupted : false
Thread-0 處于休眠中的線程被調用了 interrupt方法
******************
Thread-1線程已經被中斷
Thread-1 interrupted : false
Thread-1 中斷的線程調用了 sleep方法
*/
           

此外,還有另外一個方法也可以使目前線程釋放暫用的CPU等資源,就是yield()方法,它将通知JVM這個線程對象可以釋放CPU了。但是JVM并不保證遵循這個要求。通常來說,yield()方法隻做調試使用。

1.7 等待線程的終止

在一些情況下,我們必須等待某個線程的執行結果,才能進行接下來的操作。例如,我們的程式在執行其他的任務時,必須先初始化一些必要的資源。可以使用線程來完成這些資源的加載,等待線程終止,再執行程式的其他任務。

為了達到這個目的, 我們可以使用Thread類的join()方法。當一個線程對象的join()方法被調用時,調用這個方法的線程将被挂起,直到這個線程對象完成它的任務。

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("myThread 線程開始執行: " + System.currentTimeMillis() );
        try {
            //休眠一分鐘
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("myThread 線程執行結束: " + System.currentTimeMillis() );
    }
}
           
public class Main {
    public static void main(String[] args){
        System.out.println("main 線程開始執行: " + System.currentTimeMillis() );
        MyThread myThread = new MyThread();
        myThread.start();
        try {
            //執行這段代碼的線程,會等待 myThread線程的中止
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main 線程執行結束: " + System.currentTimeMillis() );
    }
}
/*
執行結果:
main 線程開始執行: 1593227052702
myThread 線程開始執行: 1593227052703
myThread 線程執行結束: 1593227112704
main 線程執行結束: 1593227112704
*/
           

join方法的使用方式有兩種

  • 不帶參數:join()
  • 帶參數:join(long milliseconds) 以及 join(long milliseconds, long nanos)

假設thread1中調用了thread2的join方法,如果是第一種方式調用的話,那麼thread1将會被挂起,直到thread2執行完後才會繼續運作。

如果是第二種方式調用的話,那麼thread1繼續運作的條件變成了兩個:

①thread2執行完

②等待時間超過了指定的時間

1.8 守護線程的建立和運作

Java中存在一種特殊的線程叫做守護(Daemon)線程。這種線程的優先級很低,通常來說,當同一個程式中沒有其他線程運作的時候,守護線程才會運作。

因為這種特性,守護線程通常被用來做為同一程式中的普通線程(也稱為使用者線程)的服務提供者。它們通常是無限循環的,以等待服務請求或者執行線程的任務,它們不能做重要的工作,因為我們不可能知道守護線程什麼時候能獲得CPU時鐘。守護線程的主要作用就是為使用者線程提供便利。

當同一個程式中已經沒有使用者線程在運作了,守護線程會随着JVM一起結束工作。一個典型的守護線程就是Java的垃圾回收器(Garbage Collector)。

public class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("MyThread線程已經啟動" + System.currentTimeMillis());
        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyThread線程已經結束" + System.currentTimeMillis());
    }
}
           
public class MyDaemon extends Thread {

    public MyDaemon(){
        this.setDaemon(true);
    }

    @Override
    public void run(){
        System.out.println("守護線程已經啟動" + System.currentTimeMillis());
        while (true){
            //do noting
        }
    }
}
           
public class Main {
    public static void main(String[] args){
        System.out.println("main線程已經啟動" + System.currentTimeMillis());
        MyThread myThread = new MyThread();
        myThread.start();
        MyDaemon myDaemon = new MyDaemon();
        myDaemon.start();
        System.out.println("main線程已經結束" + System.currentTimeMillis());
    }
}
/*
運作結果:
main線程已經啟動1593245216446
MyThread線程已經啟動1593245216447
main線程已經結束1593245216447
守護線程已經啟動1593245216447
MyThread線程已經結束1593245276448
*/
           

通過上面的例子我們可以發現,當main線程和myThread線程都結束後,守護線程也會随之結束。

此外,setDaemon()方法隻能在start()方法被調用之前設定,一旦線程已經開始運作了,那麼将不能再修改它的守護狀态。如果在啟動後,再修改其守護狀态,會抛出IllegalThreadStateException異常。

我們可以通過isDaemon()方法來判斷一個線程是不是守護線程。

1.9 線程中不可控制異常的處理

在Java中存在兩種異常。

  • 非運作時異常(Checked Exception):這種異常必須在方法聲明的throws中抛出,并且在調用該方法時繼續往上抛出或者使用try…catch塊捕獲。例如,IOExcption 和 ClassNotFoundException。
  • 運作時異常(Unchecked Exception):這種異常不需要在方法聲明中指定,也不需要在調用該方法時捕獲。例如:NumberFormatException。

因為run()方法不支援throws語句,也就是說run()方法不能将異常往上傳遞,那麼在run()方法中調用非運作時異常時,我們必須捕獲并進行處理。那麼當run()方法中抛出運作時異常時,jvm預設的行為是會在控制台輸出堆棧資訊,并退出程式。

但是這樣也會帶來問題,如果發生了運作時異常,線程就直接退出的話,那麼一些清理工作無法進行,有沒有什麼方法能夠讓我們處理運作時抛出的異常呢?Java提供了這樣的機制。

public class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println("MyThread 線程開始執行, " + System.currentTimeMillis());
        //此處會抛出運作時異常,NumberFormatException
        Integer num = Integer.valueOf("ttt");
        System.out.println("MyThread 線程執行結束, " + System.currentTimeMillis());
    }
}
           
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("捕獲到了一個運作時異常");
        System.out.println("線程id : " + t.getId());
        System.out.println("異常錯誤資訊如下:" + e.getMessage());
        System.out.println("堆棧資訊如下: ");
        e.printStackTrace(System.out);
        System.out.println("線程的狀态為:" + t.getState());
    }
}
           
public class Main {
    public static void main(String[] args){
        ExceptionHandler exceptionHandler = new ExceptionHandler();
        MyThread myThread = new MyThread();
        //為該線程設定一個運作時異常處理器
        myThread.setUncaughtExceptionHandler(exceptionHandler);
        myThread.start();
    }
}
/*
運作結果如下:
MyThread 線程開始執行, 1593247344833
捕獲到了一個運作時異常
線程id : 14
異常錯誤資訊如下:For input string: "ttt"
堆棧資訊如下: 
java.lang.NumberFormatException: For input string: "ttt"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at ex.MyThread.run(MyThread.java:7)
線程的狀态為:RUNNABLE
*/
           

當一個線程抛出了運作時異常,jvm會去檢測這個線程是否預設了運作時異常處理器,如果存在,那麼jvm會調用這個處理器,并将線程對象和異常傳入作為參數。

Thread類還有另一個方法可以處理運作時異常,就是靜态方法setDefaultUncaughtExceptionHandler(),該方法将為所有的線程設定一個預設的運作時異常處理器。

public class Main {
    public static void main(String[] args){
        ExceptionHandler exceptionHandler = new ExceptionHandler();
        //為所有線程設定一個預設的運作時異常處理器
        Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
        MyThread myThread = new MyThread();
//        myThread.setUncaughtExceptionHandler(exceptionHandler);
        myThread.start();
    }
}
/*
該方法運作結果與上面相同
*/
           

jvm調用運作時異常處理器的順序:

  • 首先查找該線程對象自己的運作時異常處理器
  • 如果不存在,查找線程對象所在的線程組(ThreadGroup)的運作時異常處理器
  • 如果還不存在,則使用預設的運作時異常處理器

1.10 線程局部變量的使用

共享資料是并發程式設計中最核心的問題之一。如果多個線程共享了同一個成員變量,那麼你在一個線程中改變了這個值,所有的線程都會被這個改動影響。(同樣,這些改動還将引來線程安全問題,下一篇再描述這個問題)

public class MyRunnable implements Runnable {

    private Date startTime;

    @Override
    public void run() {
        startTime = new Date();
        System.out.println(Thread.currentThread().getId() + " 線程開始,線程的startTime的值為:" + startTime);
        //讓線程随機休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId() + " 線程結束,線程的startTime的值為:" + startTime);
    }
}
           
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 3; i++){
            Thread thread = new Thread(myRunnable);
            thread.start();
            //啟動一個線程,然後main休眠兩秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
運作結果:
14 線程開始,線程的startTime的值為:Sat Jun 27 17:47:40 CST 2020
15 線程開始,線程的startTime的值為:Sat Jun 27 17:47:42 CST 2020
16 線程開始,線程的startTime的值為:Sat Jun 27 17:47:44 CST 2020
15 線程結束,線程的startTime的值為:Sat Jun 27 17:47:44 CST 2020
16 線程結束,線程的startTime的值為:Sat Jun 27 17:47:44 CST 2020
14 線程結束,線程的startTime的值為:Sat Jun 27 17:47:44 CST 2020
*/
           

通過上面這個例子我們可以發現,三個線程最後的startTime都變成了同一個值。這說明了它們共享的是同一個變量。

那麼有沒有什麼方法,能夠讓每個線程不受其他線程的影響,也就是擁有自己的線程局部變量呢?Java提供的線程局部變量機制就解決了這個問題。

public class MyRunnable implements Runnable {
	//定義線程局部變量,并重寫初始化方法
    private ThreadLocal<Date> startTime = new ThreadLocal<Date>(){
        @Override
        protected Date initialValue(){
            return new Date();
        }
    };

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getId()
                + " 線程開始,線程的startTime的值為:" + startTime.get());
        //讓線程随機休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getId()
                + " 線程結束,線程的startTime的值為:" + startTime.get());
    }
}
           
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 3; i++){
            Thread thread = new Thread(myRunnable);
            thread.start();
            //啟動一個線程,然後main休眠兩秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
運作結果如下:
14 線程開始,線程的startTime的值為:Sat Jun 27 18:21:13 CST 2020
15 線程開始,線程的startTime的值為:Sat Jun 27 18:21:15 CST 2020
16 線程開始,線程的startTime的值為:Sat Jun 27 18:21:17 CST 2020
16 線程結束,線程的startTime的值為:Sat Jun 27 18:21:17 CST 2020
14 線程結束,線程的startTime的值為:Sat Jun 27 18:21:13 CST 2020
15 線程結束,線程的startTime的值為:Sat Jun 27 18:21:15 CST 2020
*/
           

通過上面的示例,我們可以發現,每個線程的startTime值都不一樣。

線程局部變量的工作原理:

  • 線程局部變量為每個線程存儲了各自的屬性值,并提供給每個線程使用
  • 通過**get()方法讀取這個值,如果線程局部變量沒有存儲該線程的值,那麼會調用initialValue()**方法進行初始化
  • 通過**set()**方法可以設定這個值
  • 通過**remove()**方法可以删除已經存儲的值
public class MyRunnable implements Runnable {

    private ThreadLocal<Date> startTime = new ThreadLocal<Date>(){
        @Override
        protected Date initialValue(){
            return new Date();
        }
    };

    @Override
    public void run() {
        //由于線程局部變量存儲的值為空,此處調用initialValue()
        System.out.println("第一次擷取線程局部變量" + startTime.get());
        //休眠一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //調用set()方法,設定一個新的值
        Date date = new Date();
        startTime.set(date);
        System.out.println("set的date為 :" + date);
        //本次擷取的值應該和set一樣
        System.out.println("第二次擷取線程局部變量" + startTime.get());
        //休眠一秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //調用remove()方法,該方法會删除線程局部變量中儲存的值
        startTime.remove();
        //本次擷取和第一個次一樣,由于線程局部變量存儲的值為空,此處調用initialValue()
        System.out.println("第三次擷取線程局部變量" + startTime.get());
    }
}
           
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}
/*
運作結果:
第一次擷取線程局部變量Sat Jun 27 20:58:50 CST 2020
set的date為 :Sat Jun 27 20:58:51 CST 2020
第二次擷取線程局部變量Sat Jun 27 20:58:51 CST 2020
第三次擷取線程局部變量Sat Jun 27 20:58:52 CST 2020
*/
           

線程局部變量實際上是将共享變量拷貝了一份到線程的threadLocals變量(ThreadLocalMap類型)中,看這個類型名字也知道,類似于一個map結構,一個線程的所有的局部變量都緩存在其中。

InheritableThreadLocal類

InheritableThreadLocal類是ThreadLocal的子類,主要擴充了以下兩個功能:

  • 值繼承:A線程建立了B線程,那麼B線程就會繼承A線程的InheritableThreadLocal類型的線程局部變量
  • 值擴充:如果B線程實作了 **childValue()**方法,那麼會在繼承的同時,将值進行擴充
public class FatherThread extends Thread {

    @Override
    public void run(){
        System.out.println("第一次擷取父線程局部變量: " + ThreadLocalExt.str.get());
        //修改父線程局部變量的值
        ThreadLocalExt.str.set("test");
        System.out.println("第二次擷取父線程局部變量: " + ThreadLocalExt.str.get());
        SonThread sonThread = new SonThread();
        sonThread.start();
    }
}
           
public class SonThread extends Thread{

    @Override
    public void run(){
        System.out.println("子線程局部變量: " + ThreadLocalExt.str.get());
    }
}
           
public class Main {
    public static void main(String[] args){
        FatherThread fatherThread = new FatherThread();
        fatherThread.start();
    }
}
/*
執行結果:
第一次擷取父線程局部變量: 初始值
第二次擷取父線程局部變量: test
子線程局部變量: test, 子線程追加的值
*/
           

工作原理其實與ThreadLocal類似,InheritableThreadLocal是将線程局部變量拷貝了一份到Thread對象的inheritableThreadLocals中,它與threadLocals一樣,都是ThreadLocalMap類型的。在父線程中建立一個子線程時,預設的構造函數會将inheritableThreadLocals變量的值拷貝到子線程中,拷貝的方法就是**childValue()方法,是以重寫了childValue()**就能改變子線程中的值。

1.11 線程的分組

Java還提供了一個有趣的功能,可以将線程進行分組。在同一個組内的線程,可以進行統一的通路和操作。

public class MyRunnable implements Runnable {

    @Override
    public void run(){
        System.out.println("Thread start, name = " + Thread.currentThread().getName());
        //随機休眠0-100秒
        try {
            TimeUnit.SECONDS.sleep(new Random().nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
            return;
        }
        System.out.println("Thread end, name = " + Thread.currentThread().getName());
    }
}
           
public class Main {
    public static void main(String[] args){
        ThreadGroup threadGroup = new ThreadGroup("group");
        MyRunnable myRunnable = new MyRunnable();
        for (int i = 0; i < 5; i++){
            Thread thread = new Thread(threadGroup, myRunnable);
            thread.start();
            //main線程休眠兩秒
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //ThreadGroup的功能有很多,這裡示範幾個重要的
        //①列印組内所有線程
        threadGroup.list();
        //②擷取組内所有活躍線程的數量
        int count = threadGroup.activeCount();
        System.out.println("active thread : " + count);
        //③将組内所有活躍線程複制到線程數組中
        Thread[] threads = new Thread[count];
        threadGroup.enumerate(threads);
        for (Thread thread : threads){
            System.out.println("name = " + thread.getName() + " , status = " + thread.getState());
        }
        //④中斷組中所有線程,由于線程處于休眠狀态,會抛出InterruptedException異常
        threadGroup.interrupt();
    }
}
/*
運作結果:
Thread start, name = Thread-0
Thread start, name = Thread-1
Thread start, name = Thread-2
Thread start, name = Thread-3
Thread start, name = Thread-4
java.lang.ThreadGroup[name=group,maxpri=10]
    Thread[Thread-0,5,group]
    Thread[Thread-1,5,group]
    Thread[Thread-2,5,group]
    Thread[Thread-3,5,group]
    Thread[Thread-4,5,group]
active thread : 5
name = Thread-0 , status = TIMED_WAITING
name = Thread-1 , status = TIMED_WAITING
name = Thread-2 , status = TIMED_WAITING
name = Thread-3 , status = TIMED_WAITING
name = Thread-4 , status = TIMED_WAITING
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at group.MyRunnable.run(MyRunnable.java:13)
	at java.lang.Thread.run(Thread.java:748)
*/
           

除了上面顯示的這些接口外,線程組還可以設定線程的優先級,設定守護線程等等操作,線程組除了可以包含線程外,還可以包含其他的線程組,它是一個樹形結構。

線程組是通過将組内所有的線程對象和線程組對象都存儲起來,并且可以通路它們的資訊,将統一的操作執行到所有的成員上。

1.12 線程組中不可控制異常的處理

之前我們學習了,線程中如何處理不可控制的異常(也就是運作時異常),當時也提到了線程組中也可以處理運作時異常。現在就來看看,線程組中是如何處理運作時異常的。

在覆寫線程組中的uncaughtException方法,即可在其中定義運作時異常的處理,組内成員出現了運作時異常,如果沒有定義自身的異常處理器,都會進入該方法中。

public class MyRunnable implements Runnable {

    @Override
    public void run(){
        System.out.println("Thread start, name = " + Thread.currentThread().getName());
        //此處将會抛出運作時異常,NumberFormatException
        Integer num = Integer.valueOf("ttt");
        System.out.println("Thread end, name = " + Thread.currentThread().getName());
    }
}
           
public class Main {
    public static void main(String[] args){
        ThreadGroup threadGroup = new ThreadGroup("group"){
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("捕獲到了一個運作時異常");
                System.out.println("線程id : " + t.getId());
                System.out.println("異常錯誤資訊如下:" + e.getMessage());
                System.out.println("堆棧資訊如下: ");
                e.printStackTrace(System.out);
                System.out.println("線程的狀态為:" + t.getState());
            }
        };
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(threadGroup, myRunnable);
        thread.start();
    }
}
/*
Thread start, name = Thread-0
捕獲到了一個運作時異常
線程id : 14
異常錯誤資訊如下:For input string: "ttt"
堆棧資訊如下: 
java.lang.NumberFormatException: For input string: "ttt"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at group.MyRunnable.run(MyRunnable.java:12)
	at java.lang.Thread.run(Thread.java:748)
線程的狀态為:RUNNABLE
*/
           

jvm調用運作時異常處理器的順序:

  • 首先查找該線程對象自己的運作時異常處理器
  • 如果不存在,查找線程對象所在的線程組(ThreadGroup)的運作時異常處理器
  • 如果還不存在,則使用預設的運作時異常處理器

1.13 使用工廠類建立線程

工程模式是設計模式中最常用的模式之一。它是一個建造者模式,使用一個類為其他的一個類或者多個類建立對象。當我們為這些類建立對象時,不需要再使用new構造器,而直接使用工廠。

使用工廠模式,可以将對象的建立集中化,有以下幾點好處:

  • 更容易修改建立對象的方式
  • 更容易為有限資源限制建立對象的數目。例如,我們可以限制一個類隻能建立n個對象。
  • 更容易為建立的對象生産統計資料。

Java本身已經為我們提供了線程的工廠類,那就是ThreadFactory接口,這個接口實作了線程對象工廠。Java并發API的進階工具類也使用了線程工廠建立線程。

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //do noting
    }
}
           
public class MyThreadFactory implements ThreadFactory {
    /**
     * 統計建立的線程數量
     */
    private int count;
    /**
     * 統計線程建立的資訊
     */
    private List<String> stats = new ArrayList<>();

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        count++;
        stats.add("Create thread: id = " + thread.getId() + " , name = " + thread.getName());
        return thread;
    }

    public String getStatus(){
        StringBuffer stringBuffer = new StringBuffer();
        Iterator<String> iterator = stats.iterator();
        while (iterator.hasNext()){
            stringBuffer.append(iterator.next() + "\n");
        }
        return stringBuffer.toString();
    }
}
           
public class Main {
    public static void main(String[] args){
        MyRunnable myRunnable = new MyRunnable();
        MyThreadFactory myThreadFactory = new MyThreadFactory();
        for (int i = 0; i < 5; i++){
            Thread thread = myThreadFactory.newThread(myRunnable);
            thread.start();
        }
        System.out.println(myThreadFactory.getStatus());
    }
}
/*
運作結果:
Create thread: id = 14 , name = Thread-0
Create thread: id = 15 , name = Thread-1
Create thread: id = 16 , name = Thread-2
Create thread: id = 17 , name = Thread-3
Create thread: id = 18 , name = Thread-4
*/
           

ThreadFactory接口隻有一個方法,就是newThread,它以Runnable接口作為入參,傳回一個建立好的線程。當實作ThreadFactory接口時,必須實作這個方法。