jvm(12)-java內存模型與線程

【0】README
0.1)本文部分文字描述轉自「深刻理解jvm」,旨在學習 「java內存模型與線程」 的基礎知識;
 
【1】概述
1)併發處理的普遍應用是使得 Amdahl 定律代替摩爾定律稱爲計算機性能發展源動力的根本緣由;
2)Amdahl 定律:該定律經過系統中並行化與串行化的比重來描述多處理器系統能得到的運算加速能力;
3)摩爾定律:該定律用於描述處理器晶體管數量與運行效率間的發展關係;
Conclusion)這兩個定律的更替表明了近年來硬件發展從追求處理器頻率到追求多核心並行處理的發展過程;
 
【2】硬件的效率與一致性
1)高速緩存(乾貨——引入高速緩存)
  • 1.1)problem:因爲計算機的存儲設備與處理器的運算速率有幾個數量級的差距;
  • 1.2)solution:引入一層讀寫速度儘量接近處理器速度的高速緩存(cache) 來做爲內存與處理器間的緩衝: 將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩衝同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了;
2)緩存的引入產生了一個新問題——緩存一致性: 在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存,以下圖所示:
  • 2.1)問題描述(problem): 當多個處理器的運算任務都涉及到同一塊內存區域時,將可能致使各自的緩存數據不一致,那同步到內存時以誰的數據爲準呢?
  • 2.2)solution: 須要各個處理器遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有 MSI, MESI,等。
  • 2.3)內存模型: 能夠理解爲 在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象;(乾貨——java內存模型定義)
3)亂序執行: 爲了使得處理器內部的運算單元可以被儘可能使用,處理器可能對輸入代碼進行亂序執行優化,處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的;
 
【3】java內存模型
0)intro to java內存模型:java虛擬機規範試圖定義一種java內存模型(java memory model)來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都能達到一致的內存訪問效果;
【3.1】主內存與工做內存
1)java內存模型的主要目標:定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中讀取變量這樣的底層細節;
2)java內存模型規定了:全部的變量都存儲在主內存中。
  • 2.1)每條線程還有本身的工做內存: 線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取,賦值)都必須在工做內存中進行,而不能直接讀寫內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量;(乾貨——每條線程還有本身的工做內存,工做內存定義)
  • 2.2)線程間變量值的傳遞均須要經過主內存來完成:線程、內存、工做內存三者關係以下所示:
3)這裏所講的主內存,工做內存與前面講的 java內存區域中的java堆,棧,方法區等並非同一個層次的內存劃分,這二者基本沒有關係;
  • 3.1)若是硬要扯上關係,則:主內存主要對應於java堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域;
  • 3.2)更低層次上說:主內存就直接對應於物理硬件的內存,而爲了得到更好的運行速度,虛擬機可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存;
【3.2】內存間交互操做
1)關於主內存與工做內存間的交互協議,即一個變量如何從主內存拷貝到工做內存,如何從工做內存同步回主內存;java內存模型中定義瞭如下8種操做(operations):(乾貨——java內存模型中定義瞭如下8種操做)
  • o1)lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態;
  • o2)unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定;
  • o3)read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load 動做使用;
  • o4)load(載入):做用於工做內存的變量, 它把 read 操做從主內存中獲得的變量放入工做內存的變量副本中;
  • o5)use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做;
  • o6)assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做;
  • o7)store(存儲):做用於工做內存的變量, 它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用;
  • o8)write(寫入):做用於主內存的變量, 它把store操做從工做內存中獲得的變量的值放入主內存的變量中;
2)相關操做:
  • 2.1)把變量從主內存複製到工做內存:順序執行read和load操做(目的地是工做內存);
  • 2.2)把變量從工做內存同步回主內存:順序執行store和write操做(目的地是主內存);
  • Attention)java內存模型只要求上述兩個操做必須按順序執行,沒有保證是連續執行;即read和 load 之間,store和 write之間能夠插入其餘指令;(乾貨——java內存模型只要求上述兩個操做必須按順序執行,沒有保證是連續執行,它們之間還能夠插入其餘指令)
3)java內存模型還規定了在執行上述8種基本操做時必須知足以下規則(rules):
  • r1)不容許read和 load,store 和 write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現;
  • r2)不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存;
  • r3)不容許一個線程無緣由地(沒有發生過任何 assign操做)把數據從線程的工做內存同步回主內存中;
  • r4)一個新變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化的變量,換句話說,對一個變量實施 use,store操做前,必須先執行過 assign 和 load 操做;
  • r5)一個變量在同一個時刻只容許一個線程對其進行lock 操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行 lock後,只有執行相同次數的unlock 操做,變量纔會被解鎖;
  • r6)若是對一個變量執行lock 操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行 load 或 assign 操做初始化變量的值;
  • r7)若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去 unlock 一個被其餘線程鎖定住的變量;
  • r8)對一個變量執行unlock 變量前,必須先把此變量同步回主內存中(執行store, write操做);
  • Conclusion) 這8種內存訪問操做以及上述規則限定,再加上稍後介紹的對 volatile 的一些特殊規定,就已經徹底肯定了java 程序中哪些內存訪問操做在併發下是安全的;
【3】 對於volatile型變量的特殊規則
1)當一個變量定義爲volatile後,它具有兩種特性(characters):
  • c1)保證此變量對全部線程的可見性,這裏的可見性指:當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量作不到這一點,其在線程間傳遞須要經過主內存來完成;(乾貨——這就是爲何會出現數據的髒讀)
  • c2)對於volatile變量的可見性,有一些誤解: volatile變量對全部線程都是可見的,對volatile變量全部的寫操做都能馬上反應到其餘線程中,即,volatile變量在各個線程中是一致的,全部基於 volatile變量的運算在併發下是安全的。上述語句中的錯誤在於並不能得出「基於 volatile變量的運算在併發下是安全的」這個結論。
2)雖然volatile變量在各個線程的工做內存中不存在一致性問題,但java裏面的運算並不是原子操做,致使 volatile變量的運算在併發下同樣是不安全的。
  • 2.1)看個荔枝:
  • [java]  view plain  copy
     
     在CODE上查看代碼片派生到個人代碼片
    1. public class VolatileTest {  
    2.     public static volatile int race = 0;  
    3.       
    4.     public static void increase() {  
    5.         race++;  
    6.     }  
    7.       
    8.     public static final int THREADS_COUNT = 20;  
    9.       
    10.     public static void main(String[] args) {  
    11.         Thread[] threads = new Thread[THREADS_COUNT];  
    12.         for (int i = 0; i < threads.length; i++) {  
    13.             threads[i] = new Thread(new Runnable() {  
    14.                 @Override  
    15.                 public void run() {  
    16.                     for (int j = 0; j < 10000; j++) {  
    17.                         increase();  
    18.                     }  
    19.                 }  
    20.             });  
    21.             threads[i].start();  
    22.         }  
    23.           
    24.         // 等待全部累計線程都ending  
    25.         while(Thread.activeCount() > 1) {  
    26.             Thread.yield();  
    27.         }  
    28.         System.out.println(race);  
    29.     }  
    30. }  
  • 對以上執行結果的分析(Analysis):
    • A1)以上代碼的正確輸出結果是20000, 而執行的結果每次都不同,且都小於20000;
    • A2)用javap 反編譯命令獲得以下字節碼,發現increase()方法對應4條字節碼指令(return指令不算):

  • 對以上字節碼的分析(Analysis):
    • A1)當 getstatic指令 把 race 的值取到操做棧頂時,volatile關鍵字保證了 race的值在此時是正確的,可是在執行 iconst_1, iadd 這些指令的時候,其餘線程可能已經把race 的值加大了,而在操做棧頂的值就變成了過時的數據,因此 putstatic 指令執行後就可能把較小的race 值同步回主內存中;
    • A2)客觀上說,在這裏使用 字節碼來分析併發問題,不是很嚴謹。由於即便編譯出來只有一條字節碼指令,也不意味着執行這條指令就是一個原子操做。一條字節碼指令也可能會轉化成若干條本地機器碼指令,此處使用 -XX:+PrintAssembly 參數輸出反彙編來分析會更加嚴謹;(乾貨——由於即便編譯出來只有一條字節碼指令,也不意味着執行這條指令就是一個原子操做)
3)因爲volatile變量只能保證可見性,在不符合如下兩條規則的運算場景中,咱們仍然要經過加鎖來保證原子性:
  • scene1)運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值;
  • scene2)變量不須要與其餘的狀態變量共同參與不變約束;
3.1)看個荔枝(使用volatile變量來控制併發)
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. // 使用volatile變量來控制併發  
  2. public class VolatileVariableTest {  
  3.     volatile boolean shutdownRequested; // volatile變量  
  4.       
  5.     public void shutdown() {  
  6.         shutdownRequested = true;  
  7.     }  
  8.     public void doWork() {  
  9.         while(!shutdownRequested) {  
  10.             // do sth.  
  11.         }  
  12.     }  
3.2)使用volatile變量的第二個語義是禁止指令重排序優化:普通變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。由於在一個線程的 方法執行過程當中沒法感知到這點,這也就是java 內存模型中描述的所謂的 「線程內表現爲串行的語義」; (乾貨——volatile能夠禁止指令重排序優化)
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. // 指令重排序演示(僞代碼)  
  2.     public void instruct_reorder() {  
  3.         Map configOptions;  
  4.         char[] configText;  
  5.         //  此變量必須爲 volatile  
  6.         volatile boolean initialized = false;  
  7.           
  8.         // 假設如下代碼在線程A 中執行  
  9.         // 模擬讀取配置信息,當讀取完成後將 initialized 設置爲true 已通知其餘線程配置可用  
  10.         configOptions = new HashMap();  
  11.         configText = readConfigFile(filename);  
  12.         processConfigOptions(configText, configOptions);  
  13.         initialized = true;  
  14.           
  15.         // 假設如下代碼在線程B 中執行  
  16.         // 等待initialized 爲true,表明線程A 已經把配置信息初始化完成  
  17.         while(!initialized) {  
  18.             sleep();  
  19.         }  
  20.         // 使用線程A 中初始化好的配置信息  
  21.         do_sth_with_config();  
  22.     }  
對以上代碼的分析(Analysis):
  • 若是定義initialized變量沒有使用volatile修飾:就可能會因爲指令重排序的優化,致使位於線程A 中最後一句代碼「initialized=true」被提早執行;這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則能夠避免此類狀況的發生;
3.3)再看個荔枝:(分析volatile關鍵字是如何禁止指令重排序優化的),如下代碼是一段單例模式代碼,能夠觀察加入 volatile和未加入volatile關鍵字時 所生成彙編代碼的差異(如何得到JIT的彙編代碼,參考4.2.7)
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. // 單例模式(分析volatile關鍵字是如何禁止指令重排序優化的)  
  2. public class Singleton {  
  3.     private volatile static Singleton instance;  
  4.       
  5.     public static Singleton getInstance() {  
  6.         if(instance == null) {  
  7.             synchronized (Singleton.class) { // 同步塊  
  8.                 if(instance == null) {  
  9.                     instance = new Singleton();  
  10.                 }  
  11.             }  
  12.         }  
  13.         return instance;  
  14.     }  
  15.       
  16.     public static void main(String[] args) {  
  17.         Singleton.getInstance();  
  18.     }  
  19. }  
(這個反彙編荔枝待研究)
【3.4】對於long 和 double 型變量的特殊規則
1)java內存模型要求lock, unlock, read, load, assign, use,store,write這8個操做都具備原子性: 但對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:容許虛擬機將沒有被 volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現選擇能夠不保證64位數據類型的load, store,read和write這4個操做的原子性,這點就是所謂的 long 和double 的非原子性協定;
2)這項寬鬆的規定所致使的problem:若是有多個線程共享一個並未聲明爲 volatile的long 或 double類型的變量,而且同時對它們進行讀取和修改操做,那麼某些線程可能會讀取到一個既非原值,也不是其餘線程修改值的表明了半個變量的數值;
3)不過這種讀取到的「半個變量」的case很是罕見:由於java內存模型雖然容許虛擬機不把long 和 double 變量的讀寫實現成原子操做,但容許虛擬機選擇把 這些操做實現爲具備原子性的操做,並且還強烈建議虛擬機這樣實現; (乾貨——不過這種讀取到的「半個變量」的case很是罕見)
 
【3.5】原子性,可見性與有序性
0)intro: java內存模型是圍繞着在併發過程當中如何處理原子性, 可見性和有序性這3個特徵來創建的;
1)原子性:因爲java內存模型來直接保證的原子性變量操做包括 read,load,assign,use,store和write,咱們大體認爲基本數據類型的訪問讀寫數據是具有原子性的。
  • 1.1)同步塊——synchronized關鍵字:若是應用場景須要一個更大範圍的原子性保證,java內存模型還提供了lock 和 unlock 操做來知足這些需求,儘管虛擬機沒有把lock 和 unlock 操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操做;
  • 1.2)這兩個字節碼指令反映到java代碼中就是同步塊——synchronized關鍵字:所以在synchronized塊之間的操做也具有原子性;
2)可見性:可見性指當一個線程修改了共享變量的值,其餘可以當即得知這個修改。
  • 2.1)java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的,不管是普通變量仍是volatile變量都是如此;
  • 2.2)普通變量與 volatile變量的區別是:volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新;因此volatile保證了多線程操做時變量的可見性,普通變量則不能保證這一點;
  • 2.3)java還有兩個關鍵字實現可見性: synchronized 和 final;
    • 2.3.1)同步塊的可見性: 是由對一個變量執行unlock 操做前,必須先把此變量同步回主內存中;
    • 2.3.2)而final關鍵字的可見性:被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把this 的引用傳遞出去,那在其餘線程中就能看見final 字段的值。
    • 2.3.3)看個荔枝:
    • [java]  view plain  copy
       
       在CODE上查看代碼片派生到個人代碼片
      1. // final 可見性測試  
      2. public class FinalVisibilityTest {  
      3.     public static final int i;  
      4.       
      5.     public final int j;  
      6.       
      7.     static {  
      8.         i = 0;  
      9.         // do sth  
      10.     }  
      11.     {  
      12.         // 也能夠選擇在構造函數中初始化  
      13.         j = 0;  
      14.         // do sth  
      15.     }  
      16. }     
  • 對以上代碼的分析(Analysis):變量i 和 j 都具有可見性,它們無須同步就能被其餘線程正確訪問;
3)有序性
  • 3.1)java程序中自然的有序性總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指: 線程內表現爲串行的語義,後半句是指:指令重排序現象和工做內存與主內存同步延遲現象;
  • 3.2)volatile和 synchronized關鍵字保證了線程間操做的有序性:volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由 一個變量在同一時刻只容許一條線程對其進行lock 操做這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入;
【3.6】先行發生原則
1)先行發生原則定義:先行發生是 java內存模型中定義的兩項操做之間的偏序關係,若是說操做A 先行發生於操做B,其實就是說在發生操做B以前,操做A產生的影響能被操做B觀察到, 影響包括 修改了內存中共享變量的值,發送了消息,調用了方法等; (乾貨——先行發生原則定義)
2)看個荔枝(如何理解 「影響包括 修改了內存中共享變量的值,發送了消息,調用了方法等」)
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. // 先行發生 原則  
  2. public class AheadOccurTest {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.       
  6.     public void test() {  
  7.         // 如下操做在線程A中執行  
  8.         i = 1;  
  9.         // 如下操做在線程 B 中執行  
  10.         j = i;  
  11.         // 如下操做在線程 C 中執行  
  12.         i = 2;  
  13.     }  
  14. }  
對以上代碼的分析(Analysis):線程C 出如今 線程A 和 B之間, 但線程C 和 B 並無先行發生關係,那j的值 會是多少,答案是不肯定的;
3)下面是java內存模型下一些自然的先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用:
  • 3.1)程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;準確地說,應該是控制流順序;
  • 3.2)管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做;這裏必須強調的是同一個鎖,然後面是指時間上的前後順序;
  • 3.3)volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的後面是指時間上的前後順序;
  • 3.4)線程啓動規則:Thread對象的start() 方法先行發生於此線程的每個動做;
  • 3.5)線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測,能夠經過Thread.join() 方法結束,Thread.isAlive() 的返回值等手段檢測到線程已經終止運行;
  • 3.6)線程中斷規則:對線程interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 Thread.interrrupted() 方法檢測到是否有中斷髮生;
  • 3.7)對象終結規則:一個對象的初始化完成先行發生於它的finalize() 方法的開始;
  • 3.8)傳遞性:若是操做A 先行發生於操做B, 操做B 先行發生於操做C,那就能夠得出操做A 先行發生於 操做C的結論;
4)看個荔枝:如何使用這些規則去斷定操做間是否具有順序性,對於讀寫共享變量的操做來講,就是線程是否安全;
[java]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. // 利用先行發生 原則  
  2. // 斷定操做間是否具有順序性,對於讀寫共享變量的操做來講,就是線程是否安全  
  3. public class AheadOccurTest2 {  
  4.     private int value = 0;  
  5.       
  6.     public void setValue(int value) {  
  7.         this.value = value;   
  8.     }  
  9.   
  10.     public int getValue() {  
  11.         return value;  
  12.     }  
  13.       
  14.     Integer i = 0;  
  15. }     
對以上代碼的分析(Analysis):
  • A1)problem:假設線程A 先調用了 setValue(1), 以後線程B 調用了同一個對象的getValue() ,那麼線程B 收到的value是什麼?
  • A2)能夠斷定:儘管線程A在操做時間上先於線程B, 可是沒法肯定線程B 中「getValue()」 方法的返回結果,換句話說,這裏面的操做不是線程安全的;
  • A3)solution:咱們至少有兩種簡單的解決方案:
    • solution1)要麼把getter 和  setter方法都定義爲 synchronized方法,這樣就能夠套用管程鎖定規則;
    • solution2)要麼把value定義爲 volatile變量,因爲setter方法對value的修改不依賴於 value的原值,知足volatile關鍵字使用場景,這樣就能夠套用volatile變量規則來實現先行發生關係;
  • A4)得出結論:一個操做時間上的先發生 不表明這個操做會是先行發生,那若是一個操做先行發生是否就能推導出這個操做一定是 時間上的先行發生呢? (顯然推導不出來)。一個典型的荔枝就是屢次提到的「指令重排序」。
    • A4.1)看個荔枝:
    • [java]  view plain  copy
       
       在CODE上查看代碼片派生到個人代碼片
      1. // 如下操做在同一個線程中執行  
      2. int i = 1;  
      3. int j = 2;  
    • 對上述代碼的分析(Analysis): 根據程序次序規則, int i = 1 的操做先行發生於 int j =2,但 int j = 2 徹底可能先被處理器執行,這並不影響先行發生原則的正確性;
  • Conclusion)以上兩個實例得出結論:時間前後順序與先行發生原則之間基本沒有太大的關係,因此咱們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則爲準;
【4】java 與線程
【4.1】線程的實現
0)intro
  • 0.1)線程是比進程更輕量級的調度執行單位: 線程的引入,能夠把一個進程的資源分配和執行調度分開,各個線程既能夠共享進程資源(內存地址,文件IO等),又能夠獨立調度(線程是CPU 調度的基本單位);
  • 0.2)線程實現的3種方式:使用內核線程實現,使用用戶線程實現,使用用戶線程加輕量級進程混合實現(乾貨——線程實現的3種方式:使用內核線程實現+使用用戶線程實現+使用用戶線程和輕量級進程混合實現)

1)使用內核線程實現
  • 1.1)內核線程(KLT,Kernel-Level Thread):就是直接由操做系統內核(下稱內核)支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。(乾貨——內核線程和輕量級進程的定義)
  • 1.2)程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process, LWP):輕量級進程就是咱們一般意義上講的線程,因爲每一個輕量級線程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。這種輕量級進程與內核線程間1:1 的關係稱爲一對一的線程模型,以下圖所示:(乾貨——引入輕量級進程)

  • 1.3)輕量級進程有侷限性:
    • 1.3.1)首先:因爲是基於內核線程實現的,因此各類線程操做,如建立,析構及同步,都須要進行系統調用, 而系統調用的代價相對較高,須要在用戶態和內核態中來回切換;
    • 1.3.2)其次:每一個輕量級進程都須要有一個內存線程的支持,所以輕量級進程要消耗必定的內核資源(如內核線程的棧空間),所以一個系統支持輕量級進程的數量是有限的;
2)使用用戶線程實現(已經被Deprecated)
3)使用用戶線程加輕量級進程混合實現
  • 3.1)在該實現方式下,既存在用戶線程,也存在輕量級進程;
  • 3.2)用戶線程仍是徹底創建在用戶空間中:所以用戶線程的建立,切換,析構等操做依然廉價,而且能夠支持大規模的用戶線程併發;
  • 3.3)操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑:這樣可使用內核提供的線程調度功能及處理器映射,而且用戶線程的系統調用要經過輕量級線程來完成,大大下降了整個進程被徹底阻塞 的風險;(乾貨——操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑)
  • 3.4)在這種混合模式中:用戶線程與輕量級進程的數量比是不定的,即爲 N:M 的關係,以下圖所示, 這種就是多對多的線程模型;
【4.2】java線程調度
1)線程調度定義:是指系統爲線程分配處理器使用權的過程,主要調度方式有:協同式線程調度 和 搶佔式線程調度; (乾貨——線程調度定義 + 主要調度方式)
(乾貨——請注意協同式線程調度和搶佔式線程調度的區別)
2)使用協同式線程調度的多線程系統:線程的執行時間由線程自己來控制,線程把本身的工做執行完之後,要主動通知系統切換到另一個線程上;
  • 2.1)其好處是:實現簡單,並且因爲線程要把本身的事情幹完後纔會進行線程切換,切換操做讀線程本身是可知的,因此沒有什麼線程同步的問題;
  • 2.2)其壞處是:線程執行時間不可控制,甚至若是一個線程編寫有問題,一直不告訴系統進行線程切換,那麼程序就會一直阻塞在那裏;
3)使用搶佔式線程調度的多線程系統:那麼每一個線程將由系統來分配執行時間,線程的切換不禁線程自己來決定;
  • 3.1)java使用的方式就是 搶佔式線程調度方式;
  • 3.2)雖然java 線程調度是系統自動完成的: 但咱們仍是能夠建議系統給某些線程多分配一點執行時間,另一些線程則能夠少分配一點——這項操做能夠經過設置線程優先級來完成;(乾貨——設置java 線程優先級)
  • 3.3.)不過線程優先級並非太靠譜:由於java的線程是經過映射到系統的原生線程上來實現的,因此線程調度最終仍是取決於 操做系統;雖然如今不少os 都提供了線程優先級,但不見得和 能與 java線程的優先級一一對應;如 Solaris中有 2^32 種優先級,而windows只有7種 ;(乾貨——java的線程優先級並非太靠譜)
  • 3.4)下表顯示了 java線程優先級 與 windows 線程優先級之間的對應關係:
補充-Complementary):
  • C1)上文說到的「java線程優先級並非太靠譜」,不只僅是在說一些平臺上不一樣的優先級實際會變得相同這一點,還有其餘case 讓咱們不能太依賴優先級:優先級可能會被系統自行改變。(乾貨——優先級可能會被系統自行改變)
  • C2)如,在windows 中存在一個稱爲 「優先級推動器」的功能,做用是 當系統發現一個線程執行得特別勤奮的話,可能會越過線程優先級去爲它分配執行時間;
【4.3】狀態轉換
1)java定義了6種狀態(status):(乾貨——java定義了6種線程狀態)
  • 1.1)新建(New):建立後還沒有啓動的線程處於這個狀態;
  • 1.2)運行(Runnable):Runable包括了os 線程狀態中的 Running 和 Ready,也就是處於 此狀態的線程有可能正在執行,也有可能正在等待着CPU 爲它分配執行時間;
  • 1.3)無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,它們要等待被其餘線程顯式的喚醒。如下方法會讓線程陷入無限期的等待狀態(methods):
    • m1)沒有設置Timeout參數的Object.wait()方法;
    • m2)沒有設置Timeout參數的 Thread.join() 方法;
    • m3)LockSupport.park() 方法;
  • 1.4)限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU 執行時間,不過無需等待被其餘線程顯式喚醒,在必定時間以後,它們會由系統自動喚醒。如下方法會讓線程進入限期等待狀態(methods):
    • m1)Thread.sleep() 方法;
    • m2)設置了Timeout參數的Object.wait()方法;
    • m3)設置了Timeout參數的 Thread.join() 方法;
    • m4)LockSupport.parkNanos() 方法;
    • m5)LockSupport.parkUntil() 方法;
  • 1.5)阻塞(Blocked):線程被阻塞了, 阻塞狀態與等待狀態的區別是:阻塞狀態在等待着獲取到一個排他鎖,這個事件將在另一個線程放棄這個鎖的時候發生;而等待狀態則是在等待一段時間,或者喚醒動做的發生。在程序等待進入同步區域的時候, 線程將進入這種狀態;
  • 1.6)結束(Terminated):已經終止線程的線程狀態,線程已經結束執行;
2)以上6種狀態會相互轉換,轉換關係以下圖所示:
相關文章
相關標籤/搜索