1. 程式人生 > 程式設計 >什麼是執行緒、併發-J.U.C併發系列(2)

什麼是執行緒、併發-J.U.C併發系列(2)

回顧

回顧上一篇的文章,我們主要介紹了現代計算機模型,CPU的快取一致性協議,CPU和記憶體的工作原理,這些知識點都是為了更好的去學習我們的Java併發程式設計。

介紹

本文,我們來瞭解一個概念,什麼是執行緒? Java中執行緒和計算機的執行緒有什麼區別?

什麼是執行緒

現代作業系統在執行一個程式時,會為其建立一個程式。例如,啟動一個Java程式、網頁、軟體應用等,作業系統就會建立一個程式。現代作業系統排程CPU的最小單元是執行緒,也叫輕量級程式(Light Weight Process),在一個程式裡可以建立多個執行緒,這些執行緒都擁有各自的堆疊、區域性變數、計數器等屬性,並且能夠訪問共享的記憶體變數。處理器在這些執行緒上高速切換,讓我們感覺到這些執行緒在同時併發執行。

程式是系統分配資源的基本單位,執行緒是排程CPU的基本單位,一個程式至少包含一個執行執行緒(main執行緒),執行緒依附在程式當中,每個執行緒都有一組暫存器(儲存當前執行緒的工作變數)、堆疊(記錄執行歷史,其中每一幀儲存了一個已經呼叫但未返回的過程)、一個程式計數器(記錄要執行的下一條操作指令)

CPU會提供一個時間片來執行執行緒的程式碼塊,然後快速的切換不同的執行緒,以達到併發的效果。

需要知道,我們的JVM中的Thread是無法直接操作CPU的,JVM是依賴的底層的作業系統,因此會帶來一個概念,執行緒型別。

###作業系統空間

  • 核心空間
    • 系統核心,底層的程式
  • 使用者空間
    • JVM
    • eclipse應用
    • 視訊播放器

執行緒型別

  • 使用者級執行緒(User-Level Thread)
  • 核心線執行緒(Kernel-Level Thread)

執行緒級別型別

CPU級別

Intel的CPU將特權級別分為4個級別:RING0,RING1,RING2,RING3。Windows只使用其中的兩個級別RING0和RING3,RING0只給作業系統用,RING3誰都能用。如果普通應用程式企圖執行RING0指令,則Windows會顯示“非法指令”錯誤資訊。在使用者空間中,JVM會建立一個ULT級別執行緒,只能擁有Ring3級別許可權。

而Ring0級別ULT是無法去呼叫操作,至於為什麼要這樣劃分?

出發是為了安全性考慮,假如ULT可以任意去操作CPU,擁有Ring0級別,那JVM的執行緒去肆意的攻擊,修改其他的程式的指令,資料。會導致安全性問題。如果不限制,那核心裡面的指令可以被修改,病毒可以隨意的植入。

###執行緒排程

JVM假如需要生成一個核心級執行緒的話,可以怎麼操作?

可以通過呼叫核心空間提供的系統呼叫介面(JNI)去建立一個KLT級別執行緒。

建立了KLT級別執行緒之後,才可以去使用CPU,才會被分配時間片。

使用者執行緒

指不需要核心支援而在使用者程式中實現的執行緒,其不依賴於作業系統核心,應用程式利用執行緒庫提供建立、同步、排程和管理執行緒的函式來控制使用者執行緒。另外,使用者執行緒是由應用程式利用執行緒庫建立和管理,不依賴於作業系統核心。不需要使用者態/核心態切換(上下文切換),速度快。作業系統核心不知道多執行緒的存在,因此一個執行緒阻塞將使得整個程式(包括它的所有執行緒)阻塞。由於這裡的處理器時間片分配是以程式為基本單位,所以每個執行緒執行的時間相對減少。

核心執行緒

執行緒的所有管理操作都是由作業系統核心完成的。核心儲存執行緒的狀態和上下文資訊,當一個執行緒執行了引起阻塞的系統呼叫時,核心可以排程該程式的其他執行緒執行。在多處理器系統上,核心可以分派屬於同一程式的多個執行緒在多個處理器上執行,提高程式執行的並行度。由於需要核心完成執行緒的建立、排程和管理,所以和使用者級執行緒相比這些操作要慢得多,但是仍然比程式的建立和管理操作要快。大多數市場上的作業系統,如Windows,Linux等都支援核心級執行緒。

原理區分如下

我們看KLT,每個程式中的執行緒,全部依附於核心中,在核心中都會有對一個執行緒表一一對應,可以理解為輕量級的小程式,對應於具體的那個使用者空間的執行緒的具體任務,同時也擁有Ring0級別的CPU特權。

Java中建立的是哪個級別的執行緒?

  • 1.2時,建立的是ULT
  • 1.2之後,建立的是KLT
private native void start0();
複製程式碼

Java執行緒與系統核心執行緒關係

JVM建立執行緒之後,會去通過庫排程器呼叫,在核心空間中生成一個核心執行緒,並在核心空間的執行緒表關係中,進行一一對映對應。

Java建立執行緒

  1. new java.lang.Thread().start()
  2. 使用JNI將一個native thread attach到JVM中

針對 new java.lang.Thread().start()這種方式,只有呼叫start()方法的時候,才會真正的在

JVM中去建立執行緒,主要的生命週期步驟有

  1. 建立對應的JavaThread的instance
  2. 建立對應的OSThread的instance
  3. 建立實際的底層作業系統的native thread
  4. 準備相應的JVM狀態,比如ThreadLocal儲存空間分配等
  5. 底層的native thread開始執行,呼叫java.lang.Thread生成的Object的run()方法
  6. 當java.lang.Thread生成的Object的run()方法執行完畢返回後,或者丟擲異常終止後,終止native thread
  7. 釋放JVM相關的thread的資源,清除對應的JavaThread和OSThread

針對JNI將一個native thread attach到JVM中,主要的步驟有

  1. 通過JNI call AttachCurrentThread申請連線到執行的JVM例項
  2. JVM建立相應的JavaThread和OSThread物件
  3. 建立相應的java.lang.Thread的物件
  4. 一旦java.lang.Thread的Object建立之後,JNI就可以呼叫Java程式碼了
  5. 當通過JNI call DetachCurrentThread之後,JNI就從JVM例項中斷開連線
  6. JVM清除相應的JavaThread,OSThread,java.lang.Thread物件

Java執行緒的生命週期

如下圖所示

為什麼用到併發?併發會產生什麼問題?

為什麼用到併發

併發程式設計的本質其實就是利用多執行緒技術,在現代多核的CPU的背景下,催生了併發程式設計的趨勢,通過併發程式設計的形式可以將多核CPU的計算能力發揮到極致,效能得到提升。除此之外,面對複雜業務模型,並行程式會比序列程式更適應業務需求,而併發程式設計更能吻合這種業務拆分 。

即使是單核處理器也支援多執行緒執行程式碼,CPU通過給每個執行緒分配CPU時間片來實現這個機制。時間片是CPU分配給各個執行緒的時間,因為時間片非常短,所以CPU通過不停地切換執行緒執行,讓我們感覺多個執行緒是同時執行的,時間片一般是幾十毫秒(ms)。

併發不等於並行:併發指的是多個任務交替進行,而並行則是指真正意義上的“同時進行”。實際上,如果系統內只有一個CPU,而使用多執行緒時,那麼真實系統環境下不能並行,只能通過切換時間片的方式交替進行,而成為併發執行任務。真正的並行也只能出現在擁有多個CPU的系統中。

併發的優點

  1. 充分利用多核CPU的計算能力;
  2. 方便進行業務拆分,提升應用效能;

併發產生的問題

  • 高併發場景下,導致頻繁的上下文切換
  • 臨界區執行緒安全問題,容易出現死鎖的,產生死鎖就會造成系統功能不可用
  • 其它

CPU通過時間片分配演演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換。

執行緒上下文切換過程:

上下文切換

image.png

Linux為核心程式碼和資料結構預留了幾個頁框,這些頁永遠不會被轉出到磁碟上。從0x00000000 到 0xc0000000(PAGE_OFFSET) 的線性地址可由使用者程式碼 和 核心程式碼進行引用(即使用者空間)。從0xc0000000(PAGE_OFFSET)到 0xFFFFFFFFF的線性地址只能由 核心程式碼進行訪問(即核心空間)。核心程式碼及其資料結構都必須位於這 1 GB的地址空間中,但是對於此地址空間而言,更大的消費者是實體地址的虛擬對映。

這意味著在 4 GB 的記憶體空間中,只有 3 GB 可以用於使用者應用程式。一個程式只能執行在使用者方式(usermode)或核心方式(kernelmode)下。使用者程式執行在使用者方式下,而系統呼叫執行在核心方式下。在這兩種方式下所用的堆疊不一樣:使用者方式下用的是一般的堆疊,而核心方式下用的是固定大小的堆疊(一般為一個記憶體頁的大小)。

每個程式都有自己的 3 G 使用者空間,它們共享1GB的核心空間。當一個程式從使用者空間進入核心空間時,它就不再有自己的程式空間了。這也就是為什麼我們經常說執行緒上下文切換會涉及到使用者態到核心態的切換原因所在。

以上圖為例,來介紹下,CPU的上下文切換

第一步

執行緒A申請到了時間片A,執行相關的業務邏輯,當時間到達之後,CPU紙箱執行執行緒B的時間片B

這個時候執行緒A需要把一個臨時中間狀態進行儲存,以便之後繼續執行。

會把執行的結果通過CPU暫存器 ---> 快取 -- >通過bus匯流排(快取一致性協議)寫回到主記憶體中。

中間的一些狀態會存放到主記憶體中的核心空間,一個叫做Tss任務狀態段的地方,儲存了程式指令、程式指標、中間資料等。

第二步

執行時間片B,執行完之後繼續指向執行緒A的時間片A。

這個時候CPU需要重新想記憶體中load上一個時間片執行的中間結果程式指令、程式指標、中間資料。

然後重新繼續執行執行緒A的邏輯。

#小結

本文介紹了什麼是執行緒,併發,上下文切換的相關知識,希望對你有所幫助。