1. 程式人生 > 其它 >深入瞭解jvm-2Edition-Java記憶體模型與執行緒

深入瞭解jvm-2Edition-Java記憶體模型與執行緒

1、概述

  現代計算機中,CPU的運算速度遠遠快於記憶體存取速度,為了更好利用CPU的硬體效能,人們想出來很多辦法。

  1、減小CPU和記憶體的速度差距——cache

    通過在CPU和記憶體之間加入一層緩衝層,CPU的輸出輸入到cache中,輸入從cache中獲得。

    然後再利用區域性性原理,預先在cache中存入更多資料,來滿足CPU的高速處理。

    cache命中率、cache缺失、cache一致性、寫緩衝、寫回、寫直達。

  2、減少CPU等待——多工處理

    在當前任務在執行IO操作時,採用DMA方式執行IO,CPU儲存當前程序的工作現場,並切換到其他程序執行。

  在多處理器的CPU中,每個處理器都有自己的快取記憶體,這時就引出了一個問題——快取一致性(記憶體可見性)。

  當多個處理器都對同一資料進行處理時,資料在處理器的快取中可能會產生多個副本。

  Java虛擬機器規範中試圖定義一種統一的Java記憶體模型來遮蔽掉硬體和作業系統記憶體訪問的差異,從而在記憶體訪問上實現平臺無關性。

  Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機器中將變數儲存到記憶體和從記憶體取出的底層細節。

2、Java記憶體模型

  2.1 主記憶體與工作記憶體

    Java記憶體模型規定所有的變數都儲存在主記憶體中。

    每個執行緒有自己的工作記憶體,其中儲存了該執行緒使用到的變數的主記憶體副本拷貝(引用)。

    執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。

    執行緒間的變數值的傳遞均需要通過主記憶體來完成。

  2.2 記憶體間互動操作

    如何在主記憶體和工作記憶體之間傳遞資料?

    Java記憶體模型規定了8種操作:

      1、lock

        作用於主記憶體的變數,把變數標識為一條執行緒獨佔的狀態。

      2、unlock

        將主記憶體變數從鎖定狀態釋放。釋放後的變數才能被其他執行緒鎖定。

      3、read

        作用於主記憶體變數,把變數值從主記憶體傳輸入工作記憶體,以便後面的load操作使用。

      4、load

        作用於工作記憶體的變數,把read操作輸入的值放入工作記憶體的變數副本中。

      5、use

        作用於工作記憶體變數,將工作記憶體變數值傳遞給執行引擎(當虛擬機器執行到需要變數值的指令時)。

      6、assign

        作用於工作記憶體變數,把從執行引擎收到的值賦給工作記憶體變數。

      7、store

        作用於工作記憶體變數,把變數值傳送到主記憶體中。以便後面的write指令使用。

      8、write作用於主記憶體變數,把store操作傳過來的值放入主記憶體的變數中。

    執行以上8種操作需要滿足以下規則:

      1、read和load,store和write必須配對出現。不能存在輸入但不接收的情況。

      2、不允許一個執行緒丟棄它最近的assign操作,變數在工作記憶體中改變後必須同步回主記憶體。

      3、不允許一個執行緒無原因(沒有改變)地將資料同步回主記憶體。

      4、新的變數只能在主記憶體中產生,使用變數前,必須經過assign和load操作。

      5、一個變數同一時刻只能有一個執行緒對其進行lock操作。但是,可以被一個執行緒多次執行lock操作(重入)。

      6、如果對變數執行lock操作,那麼會清空工作記憶體中變數的值,使用前需要重新load或賦值。

      7、unlock必須在lock之後才能進行。

      8、對變數執行unlock之前,必須同步回記憶體。

    主記憶體--->工作記憶體:read--->load 按先後順序執行,沒有原子性要求。

    工作記憶體--->主記憶體:store--->write 按先後順序執行,沒有原子性要求。

    對於volatile型別變數的特殊規則:

      1、可見性

        一個執行緒修改了變數值,其他執行緒立即可以知道這個修改。

        沒有保證原子性。因此,read->change->write的操作(依賴當前值)是不安全的。

      2、禁止指令重排序

        在原生代碼中插入了許多記憶體屏障來保證處理器不發生亂序執行。

    對於long、double型別的特殊規則:

      非原子協定:對於64位資料型別的讀寫可以採用兩條32位指令來實現,且不強制兩條指令的原子性。

  2.3 先行發生規則

  

3、Java與多執行緒

  執行緒是比程序更輕量級的排程執行單位,執行緒的引入,可以把程序的資源分配和排程執行分開。

  各個執行緒既可以共享程序資源(記憶體、IO),又可以獨立的被排程執行。 

  3.1 執行緒實現

    1、使用核心執行緒實現

      核心執行緒(Kernal-Level Thread):直接由作業系統核心支援和管理的執行緒。

      輕量級程序(Light Weight Process):核心執行緒的介面,用於程式呼叫。

      使用核心執行緒實現就是在程式中呼叫輕量級程序介面,將執行緒的實現交給作業系統。

      LWP:KLT=1:1。

      執行緒切換代價太高,支援的執行緒數量有限。

    2、使用使用者執行緒實現

      自己在程式中實現執行緒的建立、同步、銷燬和排程。

      切換效率高,實現複雜。

    3、使用使用者執行緒+核心執行緒 的混合模式

      建立、切換銷燬自己實現,排程和處理器對映則依賴作業系統。

      UT(user thread):LWP:KLT=m:n:n。

      折中。

  3.2 Java執行緒實現

    Linux和windows都是1:1的實現。

4、Java執行緒排程

  搶佔式和非搶佔式。

  Java使用搶佔式。

  執行緒優先順序,Java有10個等級,但是最終都要對映到作業系統的優先順序上。

  Windows有7個等級。

  Java執行緒狀態:

    1、New

      建立後尚未啟動。

    2、Runable

      包括作業系統中的Running和Ready狀態。

    3、Waiting

      無限期地等待,直到被顯示地喚醒。

      沒有設Timeout引數的Object.wait()方法和Thread.join()方法;

      LockSupport.park() 方法。

    4、Timed Waiting

      一段時間後自動甦醒。

      Thread.sleep()方法;

      設定了Timeout引數的Object.wait和Thread.join方法;

      LockSupport.parkNanos()方法;

      LockSupport.parkUtil()方法。

    5、Blocked

    6、Terminated