Java线程怎样映射到操作系统线程
先說多線程模型,參考經典教材《Operating System Concepts , Silberschatz ,9th edition》
中文版是《操作系統概念,第9版》
https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html
一個線程是CPU利用率的基本單元,包括一個程序計數器,堆棧,一組寄存器和線程ID。
傳統(重量級)進程具有單個控制線程 - 有一個程序計數器,以及可在任何給定時間執行的一系列指令。
如圖4.1所示,多線程應用程序在單個進程中具有多個線程,每個線程都有自己的程序計數器,堆棧和寄存器集,但共享公共代碼,數據和某些結構(如打開文件)。
?圖4.1 - 單線程和多線程進程
多線程有四大類優點:
響應性 - 一個線程可以提供快速響應,而其他線程被阻塞或減慢進行密集計算。
資源共享 - 默認情況下,線程共享公共代碼,數據和其他資源,這允許在單個地址空間中同時執行多個任務。
經濟 - 創建和管理線程(以及它們之間的上下文切換)比為進程執行相同的任務要快得多。
可伸縮性,即多處理器體系結構的利用 - 單線程進程只能在一個CPU上運行,無論有多少可用,而多線程應用程序的執行可能在可用處理器之間分配。(請注意,當有多個進程爭用CPU時,即當負載平均值高于某個特定閾值時,單線程進程仍然可以從多處理器體系結構中受益。)
?
多核編程
計算機體系結構的最新趨勢是在單個芯片上生產具有多個核心或CPU的芯片。
在傳統的單核芯片上運行的多線程應用程序必須交錯線程,如圖4.3所示。
但是,在多核芯片上,線程可以分布在可用內核上,從而實現真正的并行處理,如圖4.4所示。
對于操作系統,多核芯片需要新的調度算法以更好地利用可用的多個核。
隨著多線程變得越來越普遍和越來越重要(數千而不是數十個線程),CPU已被開發用于支持硬件中每個核心更多的同步線程。?
?
多核芯片的挑戰
識別任務 - 檢查應用程序以查找可以同時執行的活動。
平衡 - 查找同時運行的任務,提供相同的價值。即不要浪費一些線程來完成瑣碎的任務。
數據拆分 - 防止線程相互干擾。
數據依賴性 - 如果一個任務依賴于另一個任務的結果,則需要同步任務以確保以正確的順序進行訪問。
測試和調試 - 在并行處理情況下本身就更加困難,因為競爭條件變得更加復雜和難以識別。
?
并行類型
從理論上講,有兩種不同的工作負載并行化方法:
數據并行性
在多個核(線程)之間劃分數據,并在數據的每個子集上執行相同的任務。例如,將大圖像分成多個片段并對不同核心上的每個片段執行相同的數字圖像處理。
任務并行性
劃分要在不同核心之間執行的不同任務并同時執行它們。
在實踐中,任何程序都不會僅僅由這些中的一個或另一個劃分,而是通過某種混合組合。
?
多線程模型
在現代系統中有兩種類型的線程需要管理:用戶線程和內核線程。
用戶線程由內核支持,而不需要內核管理。這些是應用程序員將在其程序中添加的線程。
內核線程由操作系統本身支持和管理。所有現代操作系統都支持內核級線程,允許內核同時執行多個同時任務或服務多個內核系統調用。
在特定實現中,必須使用以下策略之一將用戶線程映射到內核線程。
?
多對一模型
在多對一模型中,許多用戶級線程都映射到單個內核線程。
線程管理由用戶空間中的線程庫處理,這非常有效。
但是,如果進行了阻塞系統調用,那么即使其他用戶線程能夠繼續,整個進程也會阻塞。
由于單個內核線程只能在單個CPU上運行,因此多對一模型不允許在多個CPU之間拆分單個進程。
Solaris和GNU可移植線程的綠色線程在過去實現了多對一模型,但現在很少有系統繼續這樣做。
一對一模型
一對一模型創建一個單獨的內核線程來處理每個用戶線程。
一對一模型克服了上面列出的問題,涉及阻止系統調用和跨多個CPU分離進程。
但是,管理一對一模型的開銷更大,涉及更多開銷和減慢系統速度。
此模型的大多數實現都限制了可以創建的線程數。
Windows(從Win95開始)和Linux都實現了線程的一對一模型。
?
多對多模型
多對多模型將任意數量的用戶線程復用到相同或更少數量的內核線程上,結合了一對一和多對一模型的最佳特性。
用戶對創建的線程數沒有限制。
阻止內核系統調用不會阻止整個進程。
進程可以分布在多個處理器上。
可以為各個進程分配可變數量的內核線程,具體取決于存在的CPU數量和其他因素。
多對多模型的一個流行變體是雙層模型,它允許多對多或一對一操作。
IRIX,HP-UX和Tru64 UNIX使用雙層模型,Solaris 9之前的Solaris也是如此。
線程庫
線程庫為程序員提供了用于創建和管理線程的API。
線程庫可以在用戶空間或內核空間中實現。前者涉及僅在用戶空間內實現的API函數,沒有內核支持。后者涉及系統調用,并且需要具有線程庫支持的內核。
三個主要的線程庫:
POSIX Pthreads - 可以作為用戶或內核庫提供,作為POSIX標準的擴展。
Win32線程 - 在Windows系統上作為內核級庫提供。
Java線程 - 由于Java通常在Java虛擬機上運行,??因此線程的實現基于JVM運行的任何操作系統和硬件,即Pthreads或Win32線程,具體取決于系統。
?
Java線程
所有Java程序都使用Threads - 甚至是“常見的”單線程程序。
新線程的創建需要實現Runnable接口的對象,這意味著它們包含一個方法“public void run()”。Thread類的任何后代自然都會包含這樣的方法。(實際上,必須重寫/提供run()方法,以使線程具有任何實際功能。)
創建線程對象不會啟動線程運行 - 為此,程序必須調用Thread的“start()”方法。Start()為Thread分配并初始化內存,然后調用run()方法。(程序員不直接調用run()。)
因為Java不支持全局變量,所以必須將Threads傳遞給共享Object的引用才能共享數據。
請注意,JVM在本機操作系統之上運行,并且JVM規范未指定用于將Java線程映射到內核線程的模型。此決定取決于JVM實現,可能是一對一,多對多或多對一..(在UNIX系統上,JVM通常使用PThreads,而在Windows系統上,它通常使用Windows線程。)
《Java中的全局變量》Java中沒有全局變量的概念,關鍵字static定義的全局類公共字段。
?
線程池
每次需要創建新線程然后在完成時刪除它可能效率低下,并且還可能導致創建非常大(無限)的線程數。
另一種解決方案是在進程首次啟動時創建多個線程,并將這些線程放入線程池中。根據需要從池中分配線程,并在不再需要時返回池。如果池中沒有可用的線程,則該進程可能必須等到一個可用。
線程池中可用的(最大)線程數可以由可調參數確定,可能動態地響應于改變的系統負載。
Win32通過“PoolFunction”函數提供線程池。Java還通過java.util.concurrent包為線程池提供支持,Apple支持Grand Central Dispatch架構下的線程池。
信號處理
問:當多線程進程收到信號時,該信號應傳遞到哪個線程?
答:有四個主要選擇:
將信號傳遞給信號所適用的線程。
將信號傳遞給過程中的每個線程。
將信號傳遞給過程中的某些線程。
分配特定線程以接收進程中的所有信號。
最佳選擇可能取決于涉及哪個特定信號。
UNIX允許各個線程指示它們接受哪些信號以及它們忽略哪些信號。但是,信號只能傳遞給一個線程,這通常是接受該特定信號的第一個線程。
UNIX提供了兩個獨立的系統調用:kill(pid,signal)和pthread_kill(tid,signal),分別用于向進程或特定線程傳遞信號。
Windows不支持信號,但可以使用異步過程調用(APC)模擬它們。APC被傳遞到特定線程,而不是進程。
線程取消
不再需要的線程可能會被另一個線程以兩種方式之一取消:
異步取消立即取消線程。
延遲取消設置一個標志,指示線程在方便時應自行取消。然后由取消的線程定期檢查此標志,并在看到標志設置時很好地退出。
異步取消(共享)資源分配和線程間數據傳輸可能會有問題。
線程局部存儲
大多數數據在線程之間共享,這是首先使用線程的主要好處之一。
但是,有時線程也需要特定于線程的數據。
大多數主要線程庫(pThreads,Win32,Java)都支持特定于線程的數據,稱為線程本地存儲或TLS。請注意,這更像是靜態數據而不是局部變量,因為它在函數結束時不會停止存在。
?
Linux線程
Linux不區分進程和線程 - 它使用更通用的術語“Task”。
傳統的fork()系統調用完全復制了一個進程(Task),如前所述。
另一個系統調用clone()允許父和子任務之間的不同程度的共享,由下表中顯示的Flag控制:
?
調用沒有設置Flag的clone()等同于fork()。使用CLONE_FS,CLONE_VM,CLONE_SIGHAND和CLONE_FILES調用clone()等同于創建線程,因為所有這些數據結構都將被共享。
Linux使用結構task_struct實現這一點,該結構實質上為任務資源提供了間接級別。如果未設置標志,則復制結構指向的資源,但如果設置了標志,則僅復制指向資源的指針,因此共享資源。(想想深層復制與OO編程中的淺層復制。)
Linux的幾個發行版現在支持NPTL(Native POXIS Thread Library)
符合POSIX標準。
支持SMP(對稱多處理),NUMA(非統一內存訪問)和多核處理器。
支持數百到數千個線程。
?
《Linux 線程模型的比較:LinuxThreads 和 NPTL》
當 Linux 最初開發時,在內核中并不能真正支持線程。但是它的確可以通過?clone()?系統調用將進程作為可調度的實體。這個調用創建了調用進程(calling process)的一個拷貝,這個拷貝與調用進程共享相同的地址空間。LinuxThreads 項目使用這個調用來完全在用戶空間模擬對線程的支持。不幸的是,這種方法有一些缺點,尤其是在信號處理、調度和進程間同步原語方面都存在問題。另外,這個線程模型也不符合 POSIX 的要求。
要改進 LinuxThreads,非常明顯我們需要內核的支持,并且需要重寫線程庫。有兩個相互競爭的項目開始來滿足這些要求。一個包括 IBM 的開發人員的團隊開展了 NGPT(Next-Generation POSIX Threads)項目。同時,Red Hat 的一些開發人員開展了 NPTL 項目。NGPT 在 2003 年中期被放棄了,把這個領域完全留給了 NPTL。
NPTL,或稱為 Native POSIX Thread Library,是 Linux 線程的一個新實現,它克服了 LinuxThreads 的缺點,同時也符合 POSIX 的需求。與 LinuxThreads 相比,它在性能和穩定性方面都提供了重大的改進。與 LinuxThreads 一樣,NPTL 也實現了一對一的模型。
?
《著名的c10k論文》
雖然有點老,但是還是值得一讀。
注意:1:1線程與M:N線程
在實現線程庫時有一個選擇:您可以將所有線程支持放在內核中(這稱為1:1線程模型),或者您可以將其中的相當一部分移動到用戶空間(這稱為M:N線程模型)。有一點,M:N被認為是更高的性能,但它太復雜了,很難做到正確,大多數人都在遠離它。
?
《Java線程如何映射到OS線程?》
JVM線程映射到OS線程是一種常見的讀取語句。但這究竟意味著什么呢?我們在java中創建Thread對象并調用其start方法來啟動新線程。它是如何啟動OS線程的?以及如何將Thread對象的run方法附加到執行的OS線程?
調用start0方法,該方法被聲明為本機方法。“native”標記告訴JVM這是一個特定于平臺的本機方法(用C / C ++編寫),需要通過java本機接口調用。JNI是Java的本機方法接口規范,它詳細說明了本機代碼如何與JVM集成,反之亦然。(https://docs.oracle.com/javase/9??/docs/specs/jni/design.html#jni-interface-functions-and-pointers)
從Java到C++,以JVM的角度看Java線程的創建與運行
參考:【JVM源碼探秘】深入理解Thread.run()底層實現
以jdk8為例:
通過new java.lang.Thread.start()來啟動一個線程,只需要將業務邏輯放在run()方法里即可,啟動一個Java線程,調用start()方法:
在\openjdk-8u40-src-b25-10_feb_2015\openjdk\jdk\src\share\classes\java\lang\Thread.java
在啟動一個線程時會調用start0()這個native方法,關于本地方法的注冊請參照【JVM源碼探秘】深入registerNatives()底層實現
在Java的系統包下如:
java.lang.System
java.lang.Object
java.lang.Class
都有一個靜態塊用來執行一個叫做registerNatives()的native方法:
\openjdk-8u40-src-b25-10_feb_2015\openjdk\jdk\src\share\native\java\lang\Thread.c?
start0對應JVM_StartThread
VM_StartThread方法位于\openjdk-8u40-src-b25-10_feb_2015\openjdk\hotspot\src\share\vm\prims\jvm.cpp?
//分配C ++ Thread結構并創建本機線程。該
//從java檢索的堆棧大小已簽名,但構造函數需要
// size_t(無符號類型),因此請避免傳遞負值
//導致非常大的堆棧。
代碼native_thread = new JavaThread(&thread_entry, sz);用于創建JavaThread實例,位于
\openjdk-8u40-src-b25-10_feb_2015\openjdk\hotspot\src\share\vm\runtime\thread.cpp
//這里的_osthread可能為NULL,因為我們的內存不足(活動的線程太多)。
???//我們需要拋出OutOfMemoryError ?- 但是我們不能這樣做,因為調用者
???//可能會持有一個鎖,并且在拋出異常之前必須解鎖所有鎖(拋出
???//異常包括創建異常對象并初始化它,初始化
???//將通過JavaCall離開VM,然后必須解鎖所有鎖。
???//當我們到達這里時,線程仍然被暫停 線程必須顯式啟動
???//由創作者! 此外,線程還必須顯式添加到“線程”列表中
???//通過調用Threads:add。 之所以沒有這樣做,是因為線程
???//對象必須完全初始化(看看JVM_Start)?
通過OS創建線程,位于\openjdk-8u40-src-b25-10_feb_2015\openjdk\hotspot\src\os\linux\vm\os_linux.cpp
bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {assert(thread->osthread() == NULL, "caller responsible");// Allocate the OSThread objectOSThread* osthread = new OSThread(NULL, NULL);if (osthread == NULL) {return false;}// set the correct thread stateosthread->set_thread_type(thr_type);// Initial state is ALLOCATED but not INITIALIZEDosthread->set_state(ALLOCATED);thread->set_osthread(osthread);// init thread attributespthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// stack sizeif (os::Linux::supports_variable_stack_size()) {// calculate stack size if it's not specified by callerif (stack_size == 0) {stack_size = os::Linux::default_stack_size(thr_type);switch (thr_type) {case os::java_thread:// Java threads use ThreadStackSize which default value can be// changed with the flag -Xssassert (JavaThread::stack_size_at_create() > 0, "this should be set");stack_size = JavaThread::stack_size_at_create();break;case os::compiler_thread:if (CompilerThreadStackSize > 0) {stack_size = (size_t)(CompilerThreadStackSize * K);break;} // else fall through:// use VMThreadStackSize if CompilerThreadStackSize is not definedcase os::vm_thread:case os::pgc_thread:case os::cgc_thread:case os::watcher_thread:if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);break;}}stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);pthread_attr_setstacksize(&attr, stack_size);} else {// let pthread_create() pick the default value.}// glibc guard pagepthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));ThreadState state;{// Serialize thread creation if we are running with fixed stack LinuxThreadsbool lock = os::Linux::is_LinuxThreads() && !os::Linux::is_floating_stack();if (lock) {os::Linux::createThread_lock()->lock_without_safepoint_check();}pthread_t tid;int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);pthread_attr_destroy(&attr);if (ret != 0) {if (PrintMiscellaneous && (Verbose || WizardMode)) {perror("pthread_create()");}// Need to clean up stuff we've allocated so farthread->set_osthread(NULL);delete osthread;if (lock) os::Linux::createThread_lock()->unlock();return false;}// Store pthread info into the OSThreadosthread->set_pthread_id(tid);// Wait until child thread is either initialized or aborted{Monitor* sync_with_child = osthread->startThread_lock();MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag);while ((state = osthread->get_state()) == ALLOCATED) {sync_with_child->wait(Mutex::_no_safepoint_check_flag);}}if (lock) {os::Linux::createThread_lock()->unlock();}}// Aborted due to thread limit being reachedif (state == ZOMBIE) {thread->set_osthread(NULL);delete osthread;return false;}// The thread is returned suspended (in state INITIALIZED),// and is started higher up in the call chainassert(state == INITIALIZED, "race condition");return true;
}
主要是
// 調用系統庫創建線程,thread_native_entry為本地Java線程執行入口 //?
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
這個方法是C++創建線程的庫方法,通過調用這個方法,會創建一個C++ 線程并使線程進入就緒狀態,即可以開始運行
總結
以上是生活随笔為你收集整理的Java线程怎样映射到操作系统线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 钢化玻璃防爆膜价格一般是多少
- 下一篇: 一句话说清聚集索引和非聚集索引以及MyS