操作系統支持多個應用程式同時執行,每個應用至少對應一個進程,彼此之間的操作和數據不受干擾。當一個進程需要磁碟IO的時候,CPU就切換到另外的進程,提高了CPU利用率。有了進程,為什麼還要線程?因為進程的成本太高了。啟動新的進程必須分配獨立的記憶體空間,建立數據表維護它的代碼段、堆棧段和數據段,這是昂貴... ...
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源碼文件 https://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();
源碼文件 https://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}
};
在源碼文件 https://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);
}
在源碼文件 https://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
當一個應用程式需要在後臺持續做某件事情,就是守護線程的典型應用場景。比如開發一款社交軟體,開啟守護線程持續監聽聊天消息。當應用程式退出時,守護線程一定會終止。
參考文章:https://www.codingbrick.com/archives/937.html
博客作者:編碼磚家公 眾 號:編碼磚家
獨立博客:codingbrick.com
文章出處:https://www.cnblogs.com/xiaoyangjia/p/16631830.html
本文版權歸作者所有,任何人或團體、機構全部轉載或者部分轉載、摘錄,請在文章明顯位置註明作者和原文鏈接。