徹底理解線程

1 線程的意義

操作系統支持多個應用程序同時執行,每個應用至少對應一個進程,彼此之間的操作和數據不受干擾。當一個進程需要磁盤IO的時候,CPU就切換到另外的進程,提高了CPU利用率。

有了進程,為什麼還要線程?因為進程的成本太高了。

啟動新的進程必須分配獨立的內存空間,建立數據表維護它的代碼段、堆棧段和數據段,這是昂貴的多任務工作方式。如果兩個進程之間需要通信,要採用管道通信、消息隊列、共享內存等等方式。線程可以看作輕量化的進程,或者粒度更小的進程。線程之間使用相同的地址空間,切換線程的時間遠遠小於切換進程的時間。一個進程的開銷大約是線程開銷的30倍左右。

隨着操作系統的發展,進程已經演變成了線程容器的角色。進程是資源分配的最小單位,線程是CPU調度的最小單位。每一個進程中至少有一個線程,同一進程的所有線程共享該進程的所有資源。

2 詳解Java線程

我們以Java語言和JVM為例,了解一下線程的實現原理。

2.1 線程的底層實現

啟動一個Java程序會創建一個JVM進程,JVM創建、管理線程本質都是調用操作系統接口。

public class TestThreadStart {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("start thread now ");
        }, "TestThreadStart");
        thread.run();
        System.out.println("the state of thread is " + thread.getState().name());
        thread.start();
        System.out.println("the state of thread is " + thread.getState().name());
    }
}

以上代碼演示了使用start方法啟動線程,run方法只是執行同步方法,輸出結果如下:

start thread now 
the state of thread is NEW
the state of thread is RUNNABLE
start thread now 

JVM源碼文件 //github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Thread.java 中,可以看到線程啟動的start方法調用本地方法start0。

    public void start() {
        synchronized (this) {
            // zero status corresponds to state "NEW".
            if (holder.threadStatus != 0)
                throw new IllegalThreadStateException();
            start0();
        }
    }
    
    private native void start0();

源碼文件 //github.com/openjdk/jdk/blob/master/src/java.base/share/native/libjava/Thread.c 中,實現了start0方法映射到JVM本地方法。

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive0",         "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield0",           "()V",        (void *)&JVM_Yield},
    {"sleep0",           "(J)V",       (void *)&JVM_Sleep},
    {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"getStackTrace0",   "()" OBJ,     (void *)&JVM_GetStackTrace},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
    {"extentLocalCache",  "()[" OBJ,    (void *)&JVM_ExtentLocalCache},
    {"setExtentLocalCache", "([" OBJ ")V",(void *)&JVM_SetExtentLocalCache},
    {"getNextThreadIdOffset", "()J",     (void *)&JVM_GetNextThreadIdOffset}
};

在源碼文件 //github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/thread.cpp 可以看到啟動線程依賴系統級方法os::start_thread(thread)

void Thread::start(Thread* thread) {
  // Start is different from resume in that its safety is guaranteed by context or
  // being called from a Java method synchronized on the Thread object.
  if (thread->is_Java_thread()) {
    // Initialize the thread state to RUNNABLE before starting this thread.
    // Can not set it after the thread started because we do not know the
    // exact thread state at that time. It could be in MONITOR_WAIT or
    // in SLEEPING or some other state.
    java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),
                                        JavaThreadStatus::RUNNABLE);
  }
  os::start_thread(thread);
}

在源碼文件 //github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/os.cpp 找到os::start_thread方法,可以看到系統創建了線程,並且狀態設置為RUNNABLE。

void os::start_thread(Thread* thread) {
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE);
  pd_start_thread(thread);
}

Linux系統並沒有把線程和進程區別對待,無論線程還是進程都是一個數據結構,用task_struct結構體表示,唯一的區別是共享的數據區域不同。

struct task_struct {
    // 進程狀態
    long              state;
    // 虛擬內存結構體
    struct mm_struct  *mm;
    // 唯一進程號
    pid_t             pid;
    // 指向父進程的指針
    struct task_struct   *parent;
    // 子進程列表
    struct list_head      children;
    // 存放文件系統信息的指針
    struct fs_struct      *fs;
    // 進程/線程打開的文件指針
    struct files_struct   *files;
};

以上代碼是 task_struct 的極少部分字段。mm_struct是進程的虛擬內存空間,files_struct是進程將要讀寫的文件。Linux系統將一切外設和磁盤文件都當做文件處理,files_struct代表所有的IO操作。


從上圖可以看到,Linux創建進程和子進程會申請不同的內存空間,讀寫不同的文件;創建進程和進程下的線程,共享了內存空間,讀寫一樣的文件。因此多線程應用程序要利用鎖機制,避免在同一區域寫入錯亂數據的問題。

2.2 線程的生命周期

操作系統的線程生命周期可以分為五種狀態。分別是:初始狀態、可運行狀態、運行狀態、休眠狀態和終止狀態。JVM將線程等待狀態細分成兩種,一共六種狀態。

  • NEW:創建。
  • RUNNABLE:運行中。
  • BLOCKED:受阻塞並等待某個監視器鎖。
  • WAITING:無限期地等待。
  • TIMED_WAITING:等待指定時間。
  • TERMINATED:終止。
2.3 線程的優先級

操作系統調度線程有兩種方式:

  • 協作式調度:當前線程完全佔用CPU時間,執行時間由線程本身控制,直到運行結束,系統才執行下一個線程。可能出現一個線程一直佔有CPU,而其他線程等待,導致整個系統崩潰。

  • 搶佔式調度:操作系統決定下一個佔用CPU時間的是哪一個線程,定期的中斷當前正在執行的線程,任何一個線程都不能獨佔。不會因為一個線程而影響整個進程的執行,但是頻繁阻塞和調度會造成系統資源的浪費。

JVM的線程調度默認是搶佔式調度,線程調度器按照優先級決定調度哪個線程來執行。線程優先級的範圍是1~10,默認的優先級是5,10極最高。線程優先級高的不一定先執行,優先級低只是獲得調度的概率低,並不是一定最後被調度。通過setPriority()可以改變線程優先級。

2.4 JVM守護線程

守護線程是一種JVM中特殊的線程,在後台完成一些系統性的服務,比如垃圾回收。應用程序創建的線程叫做用戶線程,完成具體的業務操作。程序中所有的用戶線程執行完畢之後,不管守護線程是否結束,JVM都會自動結束。任何線程都可以通過setDaemon()設置為守護線程和用戶線程,如下代碼所示:

public class DaemonThreadDemo {

    public static void main(String[] args) {
        System.out.println("--主線程開始--");
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("執行守護線程");
            }
        });
        thread.setDaemon(true);
        thread.start();
        System.out.println("--主線程結束--");
    }
}

程序運行結果:

--主線程開始--
--主線程結束--
執行守護線程
執行守護線程
執行守護線程
執行守護線程
執行守護線程
Process finished with exit code 0

當一個應用程序需要在後台持續做某件事情,就是守護線程的典型應用場景。比如開發一款社交軟件,開啟守護線程持續監聽聊天消息。當應用程序退出時,守護線程一定會終止。

參考文章://www.codingbrick.com/archives/937.html