首頁  >  文章  >  Java  >  Java線程學習之並發程式設計知識點

Java線程學習之並發程式設計知識點

WBOY
WBOY轉載
2022-06-22 12:14:281460瀏覽

這篇文章為大家帶來了關於java的相關知識,其中主要整理了並發程式設計的相關問題,包括了Java 記憶體模型、volatile 詳解以及synchronized 的實作原理等等內容,下面一起來看一下,希望對大家有幫助。

Java線程學習之並發程式設計知識點

推薦學習:《java視訊教學

一、JMM 基礎-電腦原理

       Java 內存模型即Java Memory Model,簡稱JMM。 JMM 定義了Java 虛擬機器 (JVM)在電腦記憶體(RAM)中的工作方式。 JVM 是整個電腦虛擬模型,所以 JMM 是隸屬於 JVM 的。 Java1.5 版本對其進行了重構,現在的 Java 仍沿用了 Java1.5 的版本。 Jmm 遇到的問題與現代電腦中遇到的問題是差不多的。
        實體電腦中的同時問題,而實體機遇到的並發問題與虛擬機器中的情況有不少 相似之處,實體機對並發的處理方案對於虛擬機器的實現也有相當大的參考意義。
       根據《Jeff Dean 在Google 全體工程大會的報告》我們可以看到

Java線程學習之並發程式設計知識點

       電腦在做一些我們平時的基本作業時,需要的回應時間是不一樣的。

以下案例只做說明,並不代表真實情況。

       如果從記憶體讀取 1M 的 int 型資料由 CPU 累積,耗時多久?
       做個簡單的計算,1M 的數據,Java 裡int 型為32 位,4 個位元組,共有1024*1024/4 = 262144 個整數,則CPU 計算耗時:262144 0.6 = 157奈秒, 而我們知道從記憶體讀取1M 資料需要250000 奈秒,兩者雖然有差距(當然這個差距並不小,十萬納秒的時間足夠CPU 執行將近二十萬條指令了),但還在一個數量級上。但是,沒有任何快取機制的情況下,意味著每個數都需要從記憶體中讀取,這樣加上CPU 讀取一次記憶體需要100 奈秒,262144 個整數從記憶體讀取到CPU 加上運算時間一共需要262144100 250000 = 26 464 400 奈秒,這就存在著數量級上的差異了。

       而且現實情況中絕大多數的運算任務都不可能只靠處理器「計算」就能完成,處理器至少要與記憶體交互,如讀取運算資料、儲存運算結果等,這個I/O 操作基本上是無法消除的(無法僅靠暫存器來完成所有運算任務)。早期計算機中cpu 和內存的速度是差不多的,但在現代計算機中,cpu 的指令速度遠超內存的訪問速度,由於計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代電腦系統都必須加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為記憶體與處理器之間的緩衝:將運算需要使用到的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步回記憶體之中,這樣處理器就無須等待緩慢的記憶體讀取了。

Java線程學習之並發程式設計知識點

Java線程學習之並發程式設計知識點

       在電腦系統中,暫存器是L0 層級緩存,接著依序是L1,L2,L3(接下來是內存,本地磁碟,遠端儲存)。越往上的快取儲存空間越小,速度越快,成本也更高;越往下的儲存空間越大,速度更慢,成本也更低。從上到下,每一層都可以看做是更下一層的緩存,即:L0 寄存器是L1 一級緩存的緩存,L1 是L2 的緩存,依次類推;每一層的資料都是來至它的下一層,所以每一層的資料是下一層的資料的子集。

Java線程學習之並發程式設計知識點

       在現代CPU 上,一般為L0, L1,L2,L3 都整合在CPU 內部,而L1 也分為一級資料快取(Data Cache,D -Cache,L1d)和一級指令快取(Instruction Cache, I-Cache,L1i),分別用來存放資料和執行資料的指令解碼。每個核心擁有獨立 的運算處理單元、控制器、暫存器、L1、L2 緩存,然後一個 CPU 的多個核心共 享受最後一層 CPU 快取 L3。

二、Java 記憶體模型(JMM)

       從抽象的角度來看,JMM 定義了執行緒與主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(Main Memory )中,每個線程都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該線程以讀/寫共享變數的副本。本地記憶體是 JMM 的一個抽象概念,並不真實存在。它涵蓋了快取、寫入緩衝區、寄存器以及其他的硬體和編譯器最佳化。

Java線程學習之並發程式設計知識點

Java線程學習之並發程式設計知識點

2.1、可見性

       可見度是指當多個執行緒存取同一個變數時,一個執行緒修改了這個變數的值, 其他執行緒能夠立即看得到修改的值。
       由於線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量,那麼對於共享變量V,它們首先是在自己的工作內存,之後再同步到主內存。可是並不會及時的刷到主記憶體中,而是會有一定時間差。很明顯,這個時候線程 A 對變數 V 的操作對於線程 B 而言就不具備可見性了 。
       要解決共享物件可視性這個問題,我們可以使用 volatile 關鍵字或是加鎖。

2.2、原子性

       原子性:即一個操作或多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
       我們都知道CPU 資源的分配都是以線程為單位的,並且是分時調用,操作系統允許某個進程執行一小段時間,例如50 毫秒,過了50 毫秒操作系統就會重新選擇一個進程來執行(我們稱為「任務切換」),這個50 毫秒稱為「時間片」。而任務的切換大多數是在時間片段結束以後,。
       那麼執行緒切換為什麼會帶來 bug 呢? 因為作業系統做任務切換,可以發生在任何一條 CPU 指令執行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高階語言裡的一條語句。例如 count ,在 java 裡就是一句話,但高階語言裡一條語句往往需要多條 CPU 指令完成。其實 count 至少包含了三個 CPU 指令!

三、volatile 詳解

3.1、volatile 特性

可以把對volatile 變數的單一讀/寫,看成是使用同一個鎖對這些單一讀/寫 操作做了同步

public class Volati {


    //    使用volatile 声明一个64位的long型变量
    volatile long i = 0L;//    单个volatile 变量的读
    public long getI() {
        return i;
    }//    单个volatile 变量的写
    public void setI(long i) {
        this.i = i;
    }//    复合(多个)volatile 变量的 读/写
    public void iCount(){
        i ++;
    }}

可以看成是下面的程式碼:

public class VolaLikeSyn {

    //    使用 long 型变量
    long i = 0L;
    public synchronized long getI() {
        return i;
    }//     对单个的普通变量的读用同一个锁同步
    public synchronized void setI(long i) {
        this.i = i;
    }//    普通方法调用
    public void iCount(){
        long temp = getI();   // 调用已同步的读方法
        temp = temp + 1L;     // 普通写操作
        setI(temp);           // 调用已同步的写方法
    }}

所以volatile 變數本身就具有下列特性:

  • 可見性:對一個volatile 變數的讀,總是能看到(任意執行緒)對這個volatile 變數最後的寫入。
  • 原子性:對任何單一 volatile 變數的讀/寫具有原子性,但類似於 volatile 這種複合操作不具有原子性。

       volatile 雖然能保證執行完及時把變數刷到主記憶體中,但對於count 這種非原子性、多指令的情況,由於線程切換,線程A 剛把count=0 加載到工作內存,線程B 就可以開始工作了,這樣就會導致線程A 和B 執行完的結果都是1,都寫到主內存中,主內存的值還是1 不是2

3.2 、volatile 的實作原理

  • volatile 關鍵字修飾的變數會存在一個「lock:」的前綴。
  • Lock 前綴,Lock 不是一種記憶體屏障,但它能完成類似記憶體屏障的功能。 Lock 會對 CPU 匯流排和快取加鎖,可以理解為 CPU 指令級的一種鎖。
  • 同時指令會將目前處理器快取行的資料直接寫會到系統記憶體中,而這個寫 回記憶體的操作會使在其他 CPU 裡快取了該位址的資料無效。

四、synchronized 的實作原理

       Synchronized 在JVM 裡的實作都是基於進入和退出Monitor 物件來實現方法同步和程式碼區塊同步,雖然具體實作細節不一樣,但都可以透過成對的MonitorEnter 和MonitorExit 指令來實現。
       對同步區塊,MonitorEnter 指令插入在同步程式碼區塊的起始位置,而 monitorExit 指令則插入在方法結束處和例外處,JVM 保證每個 MonitorEnter 必須有對應的 MonitorExit。總的來說,當程式碼執行到該指令時,將會嘗試取得該物件 Monitor 的所有權,即嘗試取得該物件的鎖定:

  1. 如果 monitor 的進入數為 0,則該執行緒進入 monitor,然後將進入數設為 1,該執行緒即為 monitor 的擁有者。
  2. 如果執行緒已經佔有該 monitor,只是重新進入,則進入 monitor 的進入數加 1。
  3. 如果其他執行緒已經佔用了 monitor,則該執行緒進入阻塞狀態,直到 monitor 的進入數為 0,再重新嘗試取得 monitor 的所有權。對同步方法,從同步方法反編譯的結果來看,方法的同步並沒有透過指令 monitorenter 和 monitorexit 來實現,相對於普通方法,其常數池中多了 ACC_SYNCHRONIZED 標示符。
            JVM 就是根據該標示符來實現方法的同步的:當方法被呼叫時,調用指令將會檢查方法的ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設定了,執行線程將先獲取monitor,獲取成功之後獲取monitor,獲取成功之後獲取monitor才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何執行緒都無法再取得同一個 monitor 物件。

       synchronized 使用的鎖定是存放在Java 物件頭裡面,Java 物件的物件頭由mark word 和klass pointer 兩部分組成:

  1. mark word 儲存了同步狀態、標識、hashcode、GC 狀態等等。
  2. klass pointer 儲存物件的類型指針,指向它的類別元資料 另外對於數組而言還會有一份記錄數組長度的資料。

Java線程學習之並發程式設計知識點

鎖定資訊則是存在於物件的 mark word 中,MarkWord 裡預設資料是儲存物件的 HashCode 等資訊。

Java線程學習之並發程式設計知識點

但是會隨著物件的運作改變而變化,不同的鎖定狀態對應不同的記錄儲存方式

Java線程學習之並發程式設計知識點

4.1、鎖定的狀態

       對照上面的圖中,我們發現鎖定一共有四種狀態,無鎖定狀態,偏向鎖定狀態,輕量級鎖定狀態與重量級鎖定狀態, 它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,目的是為了提高獲得鎖和 釋放鎖的效率。

4.2、偏向鎖定

引入背景:大多數情況下鎖定不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖,減少不必要的CAS 操作。
       偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,如果在運行過程中, 同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發同步的,減少加鎖/解鎖的一些CAS 操作(例如等待隊列的一些CAS 操作),這種情況下,就會給線程加一個偏向鎖。如果在運行過程中,遇到了其他執行緒搶佔鎖,則持有​​偏向鎖的執行緒會被掛起,JVM 會消除它身上的偏向鎖,將鎖恢復到標 準的輕量級鎖。它透過消除資源無競爭情況下的同步原語,進一步提高了程式的 運作效能。

看下圖,了解偏向鎖定擷取過程:

Java線程學習之並發程式設計知識點

       步驟1、存取Mark Word 中偏向鎖定的識別碼是否設定成1,鎖標誌位是否為01,確認為可偏向狀態。
       步驟 2、 若為可偏向狀態,測試線程 ID 是否指向目前線程,如果是, 進入步驟 5,否則進入步驟 3。
       步驟 3、 若執行緒 ID 並未指向目前線程,則透過 CAS 操作競爭鎖定。如果競 爭成功,則將 Mark Word 中執行緒 ID 設定為目前執行緒 ID,然後執行 5;如果競爭 失敗,執行 4。
       步驟 4、 若 CAS 取得偏向鎖定失敗,表示有競爭。當到達全域安全點 (safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。 (撤銷偏向鎖定的時候會導致 stop the word)
       步驟 5、執行同步程式碼。

偏向鎖定的釋放:

       偏向鎖定的撤銷在上述第四步驟中被提及。偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放偏向鎖,執行緒不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域安全點(在這個時間點上沒有字節碼正在執行),它會先暫停擁有偏向鎖的線程,判斷鎖物件是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位元為“01”)或輕量級鎖定(標誌位元為“00”)的狀態。

偏向鎖定的適用場景:

       總是只有一個執行緒在執行同步區塊,在它沒有執行完釋放鎖定之前,沒有其它執行緒去執行同步區塊,在鎖無競爭的情況下使用,一旦有了競爭就升級為輕量級鎖,升級為輕量級鎖的時候需要撤銷偏向鎖,撤銷偏向鎖的時候會導致stop the word 操作;
       在當有鎖的競爭時,偏向鎖會多做很多額外操作,尤其是撤銷偏向鎖的時候會導致進入安全點,安全點會導致stw,導致效能下降,這種情況下應當停用。

jvm 開啟/關閉偏向鎖定
開啟偏向鎖定:-XX: UseBiasedLocking -XX:BiasedLockingStartupDelay=0 關閉偏向鎖定:-XX:-UseBiasedLocking

# 4.3、 輕量級鎖定

       輕量級鎖定是由偏向鎖定升級來的,偏向鎖定運作在一個執行緒進入同步區塊的情況下,當第二個執行緒加入鎖定的時候,偏向鎖定就會升級為輕量級鎖定;

輕量級鎖定的加鎖過程:

  1. 在程式碼進入同步區塊的時候,如果同步物件鎖定狀態為無鎖定狀態且不允許進行偏向(鎖定標誌位元為「01」狀態,是否為偏向鎖為「0」),虛擬機器會先在目前執行緒的堆疊中建立一個名為鎖定記錄( Lock Record)的空間,用於儲存鎖定物件目前的Mark Word 的拷貝,官方稱為Displaced Mark Word。
  2. 拷貝物件頭中的 Mark Word 複製到鎖定記錄中。
  3. 拷貝成功後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指針,並將 Lock record 裡的 owner 指針指向 object mark word。如果更新成功,則執行步驟 4,否則執行步驟 5。
  4. 如果這個更新動作成功了,那麼這個線程就擁有了該物件的鎖,並且物件Mark Word 的鎖定標誌位元設定為“00”,即表示此物件處於輕量級鎖定狀態
  5. 如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word 是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個物件的鎖,那就可以直接進入同步區塊繼續執行。否則說明多個執行緒競爭鎖,那麼它就會自旋等待鎖,一定次數後仍未獲得鎖物件。重量級線程指針指向競爭線程,競爭線程也會阻塞,等待輕量級線程釋放鎖後喚醒他。鎖標誌的狀態值變成“10”,Mark Word 中儲存 的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

4.3.1、自旋鎖定原則

       自旋鎖定原則非常簡單,如果持有鎖定的執行緒能在很短時間內釋放鎖定資源,那麼那些等待競爭鎖的執行緒就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即取得鎖,這樣就避免用戶線程和內核的切換的消耗。
       但是執行緒自旋是需要消耗 CPU 的,說白了就是讓 CPU 在做無用功,線程不能一直佔用 CPU 自旋做無用功,所以需要設定一個自旋等待的最大時間。
       如果持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的線程在最大等待時間內還是獲取不到鎖,這時爭用線程會停止自旋進入阻塞狀態。

4.3.2、自旋鎖定的優缺點

       自旋鎖定盡可能的減少執行緒的阻斷,對於鎖定的競爭並不激烈,且佔用鎖定時間非常短的程式碼區塊來說效能能大幅的提升,因為自旋的消耗會小於執行緒阻塞掛起操作的消耗。
       但是如果鎖的競爭激烈,或者持有鎖的線程需要長時間佔用鎖定執行同步區塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是佔用cpu 做無用功,佔茅坑不那啥,線程自旋的消耗大於線程阻塞掛起操作的消耗,其它需要cup 的線程又不能獲取到cpu,造成cpu 的浪費。

4.3.3、自旋鎖定時間閾值

       自旋鎖的目的是為了佔 CPU 的資源不釋放,等到取得到鎖定時立即處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的執行緒處於自旋狀態佔用 CPU 資源,進而影響整體系統的效能。因此自旋次數很重要。
       JVM 對於自旋次數的選擇,jdk1.5 預設為10 次,在1.6 引入了適應性自旋鎖, 適應性自旋鎖意味著自旋的時間不在是固定的了,而是前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本上認為一個線程上下文切換的時間是最佳的一個時間。

JDK1.6 中-XX: UseSpinning 開啟自旋鎖定; JDK1.7 後,去掉此參數,由jvm 控制;

Java線程學習之並發程式設計知識點

#4.3.4、不同鎖的比較

Java線程學習之並發程式設計知識點

推薦學習:《java影片教學

以上是Java線程學習之並發程式設計知識點的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除