Thread類、Runnable接口詳解

前言

Thread類想必都不陌生,第一次學習多線程的時候就一定會接觸Thread類。本篇主要從Thread類的定義、使用、注意事項、源碼等方面入手,全方位的講解Thread類。

Thread

我們經常會被問到這樣一個問題:

Java開啓一個新線程有哪幾種方法?

答案是兩種:繼承Thread類、實現Runnable接口。

說只有兩種,有人可能就不服了,實現Callable接口爲什麼不算?線程池爲什麼不算?
Oracle官方說明如下:
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
其中已經寫得很明白

There are two ways to create a new thread of execution.
One is to declare a class to be a subclass of Thread.This subclass should override the run method of class Thread.
The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method.

有兩種方式創建一個新的執行線程
一種是定義Thread的子類,子類重寫run方法
另一種是定義Runnable接口的實現類,實現run方法

至於爲什麼實現Callable接口和線程池不算,以後的博客會詳細介紹。

一個小問題相信已經讓大家回憶起了Thread類相關的知識,接下來就從源碼的角度解析Thread

定義

Thread類從JDK1.0版本開始就有了,可謂是歷史悠久。本篇以JDK1.8爲例進行源碼講解,Thread類定義如下(只列出需要重點關注的成員變量、常量):

// Thread類實現了Runnable接口
public class Thread implements Runnable {

	// 是否是守護線程
    private boolean daemon = false;
    
   // 最小優先級
    public final static int MIN_PRIORITY = 1;

   // 默認優先級
    public final static int NORM_PRIORITY = 5;

    // 最大優先級
    public final static int MAX_PRIORITY = 10;

	// 線程名稱
    private volatile char  name[];
    // 線程優先級
    private int priority;

	// 需要執行的單元
    private Runnable target;

	// 線程狀態
	private volatile int threadStatus = 0;

	// 線程ID
	private long tid;
}

Thread類的定義可以看出,對於一個Thread,需要重點關注的有以下幾點:

  • 實現了Runnable接口
  • 線程需要重點關注的四個屬性:ID、Name、是否是守護線程、優先級
  • 線程的狀態需要特別注意

接下來就從這三點分別進行詳細講解,因爲線程的狀態之前已經專門寫過一篇博客:Java線程到底有幾種狀態。所以重點講解其餘的兩點

實現Runnable接口

前文說到Java開啓一個新線程的兩種方式:繼承Thread類,重寫run方法;實現Runnable接口,實現run方法。接下來就來看一下Thread類中的run方法:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

其中targetRunnable類型的引用,也可以看做線程的執行單元,結合下面一個小實例:

/**
 * @author sicimike
 */
public class CreateThreadDemo {

    public static void main(String[] args) {
        new SicThread1().start();
        new Thread(new SicThread2()).start();
    }

}

class SicThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("extends Thread");
    }
}

class SicThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println("implements Runnable");
    }
}

在代碼

new SicThread1().start();

中調用的SicThread1start方法,間接調用重寫的run方法。

SicThread2直接實現了Runnable接口,在代碼

new Thread(new SicThread2()).start();

中調用的是構造方法public Thread(Runnable target) {...}
由於Thread類實現了Runnable接口,相當於SicThread1也實現了Runnable接口,所以也可以寫new Thread(new SicThread1()).start();這樣的代碼來啓動線程。

也就是說,不管是繼承Thread類還是實現Runnable接口,都是利用Thread類的run方法。只是前者是重寫了Thread類的run方法,後者是給Thread類傳遞一個Runnable target,調用targetrun方法。至於這兩種方法本質上算不算同一種,這就“仁者見仁,智者見智”了。既然Oracle認爲是兩種,那還是以官方描述爲準。

那這兩種方式,哪一種更好
毫無疑問,實現Runnable接口更好,理由有三:

  • 解耦角度:Runnable接口只定義了一個抽象方法run,語義非常明確,就是線程需要執行的任務。而Thread類除了線程需要執行的任務,還需要維護線程的生命週期、狀態轉換等
  • 資源角度:繼承Thread類的方式,如果想要執行一個任務,必須新建一個線程,執行完成後還要銷燬,開銷非常大;而實現Runnable接口只需要新建任務,可以做到同一個線程執行多個任務,大大減小了線程創建、銷燬的資源浪費
  • 擴展角度:Java不支持多繼承,一個類如果繼承了Thread類就不能再繼承別的類,不利於未來的擴展

四個屬性

屬性 用途/說明
ID(Long) 唯一標識不同的線程
Name(char[]) 線程名稱,用於調試 、定位問題等
daemon(boolean) 是否是守護線程,true表示是守護線程,false表示非守護線程(用戶線程)
priority(int) 用於告訴CPU哪些線程希望被更多的執行,哪些線程希望被更少的執行

線程ID

線程ID從1(主線程)開始自增,(程序)不能手動修改。現在看下Thread類中關於ID的部分:

// 線程ID
private long tid;

public long getId() {
    return tid;
}

// jdk1.8.0_101版本,第422行
// 設置線程ID
/* Set thread ID */
tid = nextThreadID();

// 用於生成線程ID
private static long threadSeqNumber;

// 加鎖的自增操作
private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

從源碼可以看出ID的兩個特點:

  • 從1開始自增
  • 不能手動修改

線程Name

看了線程ID相關的源碼後,很容易就總結除了線程ID相關的特點。所以同樣看下Thread類關於Name的重要操作:

// 不傳入名字時,默認就是"Thread-" + 數字
public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target, String name) {
init(null, target, name, 0);
}

// 用於匿名線程編號
private static int threadInitNumber;

// 加鎖的自增操作
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

// 可以動態設置線程name
public final synchronized void setName(String name) {
	// 確定當前線程有修改該線程的權限
    checkAccess();
    // 設置線程名字
    this.name = name.toCharArray();
    if (threadStatus != 0) {
    	// 如果線程不是處於0(線程未啓動)狀態,則不能修改native層的name
        setNativeName(name);
    }
}

private native void setNativeName(String name);

至此,可以總結出關於線程Name的兩個特點:

  • 默認線程名稱是"Thread-" + 數字(從0開始),爲了方便調試,應該給每個線程取一個有意義的名字
  • 實例化時如果沒有設置線程Name,之後還可以通過setName的方式設置線程Name

守護線程

守護線程的主要作用是爲了給用戶線程提供一系列服務,守護線程有三個特點:

  • 線程類型默認繼承自父線程:守護線程創建的線程默認就是守護線程;用戶線程創建的線程默認就是用戶線程,可以通過setDaemon方法修改這個屬性
  • 守護線程一般由JVM啓動
  • 守護線程不影響JVM的退出

守護線程和用戶線程本質上沒有多大區別,最大的區別就是守護線程不影響JVM的退出。

線程優先級

Java中定義的線程優先級有1-10(十個等級,數值越大,優先級越高),默認爲5。雖然Thread類定義優先級這個功能,但是程序的設計不應該依賴於優先級。究其原因,主要有兩點:

  • Thread類中定義的優先級不代表操作系統的優先級,不同的操作系統有不同的優先級定義
  • 優先級可能被操作系統改變

核心方法

瞭解了Thread的定義及核心屬性後,再來看看Thread的核心方法startsleepjoinyield

start方法

啓動一個線程的方式就是調用它的start()方法,而不是run()方法。有時也會被問到這樣兩個問題:

同一個線程兩次(多次)調用start方法會怎樣?
啓動一個線程爲什麼不能調用run方法,而是start方法?

看完start方法的實現,能輕鬆回答這兩個問題,下面是start方法的實現

public synchronized void start() {

   if (threadStatus != 0)
   		// 如果線程狀態不是“未啓動”,會拋出IllegalThreadStateException異常
   		// 這裏就回答了上面的第一個問題
       throw new IllegalThreadStateException();

   // 加入線程組
   group.add(this);

	// 線程是否已經啓動,啓動後設置成true
   boolean started = false;
   try {
       start0();
       started = true;
   } finally {
       try {
           if (!started) {
           	   // 啓動失敗,把線程從線程組中刪除
               group.threadStartFailed(this);
           }
       } catch (Throwable ignore) {
           /* do nothing. If start0 threw a Throwable then
             it will be passed up the call stack */
       }
   }
}

// 真正的啓動線程的方法(native方法)
private native void start0();

根據start方法的實現可以總結出start做了哪些邏輯:

  • 檢查線程狀態
  • 加入線程組
  • 調用native方法start0通知JVM啓動一個新線程
  • 如果啓動失敗,從線程組中刪除線程

再來回顧下Threadrun方法的實現:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

對比這兩個方法就可看出,start方法爲線程的啓動做了一系列準備,再去通知JVM啓動一個新線程;而run方法僅僅是一個普通方法,所以不能啓動一個新線程。

sleep方法

sleep(long millis)方法的作用是讓線程休眠指定的時間,在指定時間內不佔用CPU資源。sleep方法的特點有以下幾點:

  • 線程處於TIMED_WAITING狀態
  • sleep期間不佔用CPU資源
  • sleep期間不釋放鎖(Synchronized鎖和ReentrantLock都不釋放)
  • sleep方法能響應中斷,檢測到中斷後拋出InterruptedException然後清除中斷狀態

join方法

join的作用是阻塞當前線程等待加入的線程執行完成後再繼續執行。使用這個方法一定要清楚是哪個線程被阻塞,舉個例子:

/**
 * 使用join方法
 * @author sicimike
 */
public class ThreadJoinDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            // 可以在此適當的休眠,使結果更清晰
            System.out.println("sub thread");
        });

        thread.start();
        try {
            // join方法
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main thread");
    }
}

執行結果

sub thread
main thread

在主線程中調用thread.join(),結果子線程先輸出,主線程後輸出。這個結果是確定的,不存在隨機性。這就是join方法的作用,主線程中調用子線程的join方法,阻塞的主線程,等待子線程執行完成後,主線程繼續執行。
日常編碼中應該儘量避免使用join方法,而是使用JDK封裝好的併發工具CountDownLatchCyclicBarrier代替:併發工具三巨頭CountDownLatch、CyclicBarrier、Semaphore使用

接下來看下join方法的實現:

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
    	// 未設置超時時間,一直被阻塞
        while (isAlive()) {
        	// 線程處於可用狀態(既不是NEW,也不是TERMINATED)
        	// 永久阻塞
            wait(0);
        }
    } else {
        while (isAlive()) {
        	// 線程處於可用狀態(既不是NEW,也不是TERMINATED)
        	// 計算剩餘的阻塞時間
            long delay = millis - now;
            if (delay <= 0) {
            	// 阻塞時間已經到了
                break;
            }
            // 阻塞時間未到,阻塞指定時間
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

通過源碼可以看出,Thread類中的阻塞是通過wait方法實現的。
值得注意的是:整個方法執行結束也沒有執行notify或者notifyAll方法。因爲Thread類的run執行結束後,會自動執行notifyAll方法。這也是Thread類不適合作爲鎖對象的原因。

join方法的特點有以下幾點:

  • 線程處於WAITING或者TIMED_WAITING狀態
  • 底層調用的wait方法
  • 能響應中斷,檢測到中斷後拋出InterruptedException然後清除中斷狀態

yield方法

yield方法的作用是釋放CPU時間片,然後重新競爭。該方法不會釋放鎖,也不會改變線程狀態,線程始終處於RUNNABLE狀態。

停止線程

如何停止線程是一個比較大的話題,之前特意單獨拿出來寫過: 如何優雅的中斷線程 ,此處就不再贅述。

總結

本篇主要深入源碼,結合實例較爲完整的講解了JDK中的線程Thread類。具體講解的內容有線程的類定義、成員變量/常量、核心屬性、核心方法、線程啓動、線程終止等。

以上便是本篇的全部內容。

發佈了55 篇原創文章 · 獲贊 107 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章