JVM掃盲:虛擬機(jī)內(nèi)存模型與高效并發(fā)

當(dāng)多個(gè)線程訪問同一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替運(yùn)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其他的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲取正確的結(jié)果,那這個(gè)對(duì)象是線程安全的。 關(guān)于定義的理解這是一個(gè)仁者見仁智者見智的事情。出現(xiàn)線程安全的問題一般是因?yàn)橹鲀?nèi)存和工作內(nèi)存數(shù)據(jù)不一致性和重排序?qū)е碌?,而解決線程安全的問題最重要的就是理解這兩種問題是怎么來的,那么,理解它們的核心在于理解java內(nèi)存模型(JMM)。

1、虛擬機(jī)內(nèi)存模型 JMM

Java內(nèi)存模型,即Java Memory Model,簡稱JMM,它是一種抽象的概念,或者是一種協(xié)議,用來解決在并發(fā)編程過程中內(nèi)存訪問的問題,同時(shí)又可以兼容不同的硬件和操作系統(tǒng),JMM的原理與硬件一致性的原理類似。在硬件一致性的實(shí)現(xiàn)中,每個(gè)CPU會(huì)存在一個(gè)高速緩存,并且各個(gè)CPU通過與自己的高速緩存交互來向共享內(nèi)存中讀寫數(shù)據(jù)。

如下圖所示,在Java內(nèi)存模型中,所有的變量都存儲(chǔ)在主內(nèi)存。每個(gè)Java線程都存在著自己的工作內(nèi)存,工作內(nèi)存中保存了該線程用得到的變量的副本,線程對(duì)變量的讀寫都在工作內(nèi)存中完成,無法直接操作主內(nèi)存,也無法直接訪問其他線程的工作內(nèi)存。當(dāng)一個(gè)線程之間的變量的值的傳遞必須經(jīng)過主內(nèi)存。

當(dāng)兩個(gè)線程A和線程B之間要完成通信的話,要經(jīng)歷如下兩步:

  1. 線程A從主內(nèi)存中將共享變量讀入線程A的工作內(nèi)存后并進(jìn)行操作,之后將數(shù)據(jù)重新寫回到主內(nèi)存中;
  2. 線程B從主存中讀取最新的共享變量

volatile關(guān)鍵字使得每次volatile變量都能夠強(qiáng)制刷新到主存,從而對(duì)每個(gè)線程都是可見的。

<figcaption></figcaption>

需要注意的是,JMM與Java內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當(dāng)說JMM描述的是一組規(guī)則,通過這組規(guī)則控制程序中各個(gè)變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問方式。在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個(gè)程度上講應(yīng)該包括了堆和方法區(qū),而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個(gè)程度上講則應(yīng)該包括程序計(jì)數(shù)器、虛擬機(jī)棧以及本地方法棧。

內(nèi)存間交互的操作

上面介紹了JMM中主內(nèi)存和工作內(nèi)存交互以及線程之間通信的原理,但是具體到各個(gè)內(nèi)存之間如何進(jìn)行變量的傳遞,JMM定義了8種操作,用來實(shí)現(xiàn)主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議:

  1. lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài);
  2. unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定;
  3. read(讀?。鹤饔糜谥鲀?nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用;
  4. load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中;
  5. use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作;
  6. assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作;
  7. store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作;
  8. write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。

如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行readload操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行storewrite操作。Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。也就是readload之間,storewrite之間是可以插入其他指令的,如對(duì)主內(nèi)存中的變量ab進(jìn)行訪問時(shí),可能的順序是read a,read b,load b, load a。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時(shí),必須滿足如下規(guī)則:

  1. 不允許readload、storewrite操作之一單獨(dú)出現(xiàn);
  2. 不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中;
  3. 不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中;
  4. 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施usestore操作之前,必須先執(zhí)行過了assignload操作;
  5. 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行lock操作,lockunlock必須成對(duì)出現(xiàn);
  6. 如果對(duì)一個(gè)變量執(zhí)行lock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行loadassign操作初始化變量的值;
  7. 如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定的變量;
  8. 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行storewrite操作)。

此外,虛擬機(jī)還對(duì)voliate關(guān)鍵字和long及double做了一些特殊的規(guī)定。

voliate關(guān)鍵字的兩個(gè)作用

  1. 保證變量的可見性:當(dāng)一個(gè)被voliate關(guān)鍵字修飾的變量被一個(gè)線程修改的時(shí)候,其他線程可以立刻得到修改之后的結(jié)果。當(dāng)一個(gè)線程向被voliate關(guān)鍵字修飾的變量寫入數(shù)據(jù)的時(shí)候,虛擬機(jī)會(huì)強(qiáng)制它被值刷新到主內(nèi)存中。當(dāng)一個(gè)線程用到被voliate關(guān)鍵字修飾的值的時(shí)候,虛擬機(jī)會(huì)強(qiáng)制要求它從主內(nèi)存中讀取。
  2. 屏蔽指令重排序:指令重排序是編譯器和處理器為了高效對(duì)程序進(jìn)行優(yōu)化的手段,它只能保證程序執(zhí)行的結(jié)果時(shí)正確的,但是無法保證程序的操作順序與代碼順序一致。這在單線程中不會(huì)構(gòu)成問題,但是在多線程中就會(huì)出現(xiàn)問題。非常經(jīng)典的例子是在單例方法中同時(shí)對(duì)字段加入voliate,就是為了防止指令重排序。為了說明這一點(diǎn),可以看下面的例子。

我們以下面的程序?yàn)槔齺碚f明voliate是如何防止指令重排序:

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) { // 1
            sychronized(Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(); // 2
                }
            }
        }
        return singleton;
    }
} 
復(fù)制代碼

實(shí)際上當(dāng)程序執(zhí)行到2處的時(shí)候,如果我們沒有使用voliate關(guān)鍵字修飾變量singleton,就可能會(huì)造成錯(cuò)誤。這是因?yàn)槭褂?code>new關(guān)鍵字初始化一個(gè)對(duì)象的過程并不是一個(gè)原子的操作,它分成下面三個(gè)步驟進(jìn)行:

  1. 給 singleton 分配內(nèi)存
  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
  3. 將 singleton 對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton 就為非 null 了)

如果虛擬機(jī)存在指令重排序優(yōu)化,則步驟2和3的順序是無法確定的。如果A線程率先進(jìn)入同步代碼塊并先執(zhí)行了3而沒有執(zhí)行2,此時(shí)因?yàn)閟ingleton已經(jīng)非null。這時(shí)候線程B到了1處,判斷singleton非null并將其返回使用,因?yàn)榇藭r(shí)Singleton實(shí)際上還未初始化,自然就會(huì)出錯(cuò)。

但是特別注意在jdk 1.5以前的版本使用了volatile的雙檢鎖還是有問題的。其原因是Java 5以前的JMM(Java 內(nèi)存模型)是存在缺陷的,即時(shí)將變量聲明成volatile也不能完全避免重排序,主要是volatile變量前后的代碼仍然存在重排序問題。這個(gè)volatile屏蔽重排序的問題在jdk 1.5 (JSR-133)中才得以修復(fù),這時(shí)候jdk對(duì)volatile增強(qiáng)了語義,對(duì)volatile對(duì)象都會(huì)加入讀寫的內(nèi)存屏障,以此來保證可見性,這時(shí)候2-3就變成了代碼序而不會(huì)被CPU重排,所以在這之后才可以放心使用volatile。

對(duì)long及double的特殊規(guī)定

虛擬機(jī)除了對(duì)voliate關(guān)鍵字做了特殊規(guī)定,還對(duì)long及double做了一些特殊的規(guī)定:允許沒有被volatile修飾的long和double類型的變量讀寫操作分成兩個(gè)32位操作。也就是說,對(duì)long和double的讀寫是非原子的,它是分成兩個(gè)步驟來進(jìn)行的。但是,你可以通過將它們聲明為voliate的來保證對(duì)它們的讀寫的原子性。

先行發(fā)生原則(happens-before) & as-if-serial

Java內(nèi)存模型是通過各種操作定義的,JMM為程序中所有的操作定義了一個(gè)偏序關(guān)系,就是先行發(fā)生原則(Happens-before)。它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的主要依據(jù)。想要保證執(zhí)行操作B的線程看到操作A的結(jié)果,那么在A和B之間必須滿足Happens-before關(guān)系,否則JVM就可以對(duì)它們?nèi)我獾嘏判颉?/p>

先行發(fā)生原則主要包括下面幾項(xiàng),當(dāng)兩個(gè)變量之間滿足以下關(guān)系中的任意一個(gè)的時(shí)候,我們就可以判斷它們之間的是存在先后順序的,串行執(zhí)行的。

  1. 程序次序規(guī)則(Program Order Rule):在同一個(gè)線程中,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操縱。準(zhǔn)確的說是程序的控制流順序,考慮分支和循環(huán)等;
  2. 管理鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面(時(shí)間上的順序)對(duì)同一個(gè)鎖的lock操作;
  3. volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面(時(shí)間上的順序)對(duì)該變量的讀操作;
  4. 線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作;
  5. 線程終止規(guī)則(Thread Termination Rule):線程的所有操作都先行發(fā)生于對(duì)此線程的終止檢測,可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行;
  6. 線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時(shí)事件的發(fā)生。Thread.interrupted()可以檢測是否有中斷發(fā)生;
  7. 對(duì)象終結(jié)規(guī)則(Finilizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()的開始;
  8. 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么可以得出A先行發(fā)生于操作C。

不同操作時(shí)間先后順序與先行發(fā)生原則之間沒有關(guān)系,二者不能相互推斷,衡量并發(fā)安全問題不能受到時(shí)間順序的干擾,一切都要以先行發(fā)生原則為準(zhǔn)。

如果兩個(gè)操作訪問同一個(gè)變量,且這兩個(gè)操作有一個(gè)為寫操作,此時(shí)這兩個(gè)操作就存在數(shù)據(jù)依賴性這里就存在三種情況:1).讀后寫;2).寫后寫;3). 寫后讀,三種操作都是存在數(shù)據(jù)依賴性的,如果重排序會(huì)對(duì)最終執(zhí)行結(jié)果會(huì)存在影響。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴性關(guān)系的兩個(gè)操作的執(zhí)行順序。

還有就是as-if-serial語義:不管怎么重排序(編譯器和處理器為了提供并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語義。as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。

先行發(fā)生原則(happens-before)和as-if-serial語義是虛擬機(jī)為了保證執(zhí)行結(jié)果不變的情況下提供程序的并行度優(yōu)化所遵循的原則,前者適用于多線程的情形,后者適用于單線程的環(huán)境。

2、Java線程

2.1 Java線程的實(shí)現(xiàn)

在Window系統(tǒng)和Linux系統(tǒng)上,Java線程的實(shí)現(xiàn)是基于一對(duì)一的線程模型,所謂的一對(duì)一模型,實(shí)際上就是通過語言級(jí)別層面程序去間接調(diào)用系統(tǒng)內(nèi)核的線程模型,即我們?cè)谑褂肑ava線程時(shí),Java虛擬機(jī)內(nèi)部是轉(zhuǎn)而調(diào)用當(dāng)前操作系統(tǒng)的內(nèi)核線程來完成當(dāng)前任務(wù)。這里需要了解一個(gè)術(shù)語,內(nèi)核線程(Kernel-Level Thread,KLT),它是由操作系統(tǒng)內(nèi)核(Kernel)支持的線程,這種線程是由操作系統(tǒng)內(nèi)核來完成線程切換,內(nèi)核通過操作調(diào)度器進(jìn)而對(duì)線程執(zhí)行調(diào)度,并將線程的任務(wù)映射到各個(gè)處理器上。每個(gè)內(nèi)核線程可以視為內(nèi)核的一個(gè)分身,這也就是操作系統(tǒng)可以同時(shí)處理多任務(wù)的原因。由于我們編寫的多線程程序?qū)儆谡Z言層面的,程序一般不會(huì)直接去調(diào)用內(nèi)核線程,取而代之的是一種輕量級(jí)的進(jìn)程(Light Weight Process),也是通常意義上的線程,由于每個(gè)輕量級(jí)進(jìn)程都會(huì)映射到一個(gè)內(nèi)核線程,因此我們可以通過輕量級(jí)進(jìn)程調(diào)用內(nèi)核線程,進(jìn)而由操作系統(tǒng)內(nèi)核將任務(wù)映射到各個(gè)處理器,這種輕量級(jí)進(jìn)程與內(nèi)核線程間1對(duì)1的關(guān)系就稱為一對(duì)一的線程模型。

如圖所示,每個(gè)線程最終都會(huì)映射到CPU中進(jìn)行處理,如果CPU存在多核,那么一個(gè)CPU將可以并行執(zhí)行多個(gè)線程任務(wù)。

2.2 線程安全

Java中可以使用三種方式來保障程序的線程安全:1).互斥同步;2).非阻塞同步;3).無同步。

互斥同步

在Java中最基本的使用同步方式是使用sychronized關(guān)鍵字,該關(guān)鍵字在被編譯之后會(huì)在同步代碼塊前后形成monitorentermonitorexit字節(jié)碼指令。這兩個(gè)字節(jié)碼都需要一個(gè)reference類型的參數(shù)來指明要鎖定和解鎖的對(duì)象。如果在Java程序中明確指定了對(duì)象參數(shù),就會(huì)使用該對(duì)象,否則就會(huì)根據(jù)sychronized修飾的是實(shí)例方法還是類方法,去去對(duì)象實(shí)例或者Class對(duì)象作為加鎖對(duì)象。

synchronized先天具有重入性:根據(jù)虛擬機(jī)的要求,在執(zhí)行sychronized指令時(shí),首先要嘗試獲取對(duì)象的鎖。如果這個(gè)對(duì)象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了該對(duì)象的鎖,就把鎖的計(jì)數(shù)器加1,相應(yīng)地執(zhí)行monitorexit指令時(shí)會(huì)將鎖的計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí)就釋放鎖。弱獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另外一個(gè)線程釋放為止。

除了使用sychronized,我們還可以使用JUC中的ReentrantLock來實(shí)現(xiàn)同步,它與sychronized類似,區(qū)別主要表現(xiàn)在以下3個(gè)方面:

  1. 等待可中斷:當(dāng)持有鎖的線程長期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待;
  2. 公平鎖:多個(gè)線程等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖;而非公平鎖無法保證,當(dāng)鎖被釋放時(shí)任何在等待的線程都可以獲得鎖。sychronized本身時(shí)非公平鎖,而ReentrantLock默認(rèn)是非公平的,可以通過構(gòu)造函數(shù)要求其為公平的。
  3. 鎖可以綁定多個(gè)條件:ReentrantLock可以綁定多個(gè)Condition對(duì)象,而sychronized要與多個(gè)條件關(guān)聯(lián)就不得不加一個(gè)鎖,ReentrantLock只要多次調(diào)用newCondition即可。

在JDK1.5之前,sychronized在多線程環(huán)境下比ReentrantLock要差一些,但是在JDK1.6以上,虛擬機(jī)對(duì)sychronized的性能進(jìn)行了優(yōu)化,性能不再是使用ReentrantLock替代sychronized的主要因素。

非阻塞同步

所謂非阻塞同步就是在實(shí)現(xiàn)同步的過程中無需將線程掛起,它是相對(duì)于互斥同步而言的。互斥同步本質(zhì)上是一種悲觀的并發(fā)策略,而非阻塞同步是一種樂觀的并發(fā)策略。在JUC中的許多并發(fā)組建都是基于CAS原理實(shí)現(xiàn)的,所謂CAS就是Compare-And-Swape,類似于樂觀加鎖。但與我們熟知的樂觀鎖不同的是,它在判斷的時(shí)候會(huì)涉及到3個(gè)值:“新值”、“舊值”和“內(nèi)存中的值”,在實(shí)現(xiàn)的時(shí)候會(huì)使用一個(gè)無限循環(huán),每次拿“舊值”與“內(nèi)存中的值”進(jìn)行比較,如果兩個(gè)值一樣就說明“內(nèi)存中的值”沒有被其他線程修改過,否則就被修改過,需要重新讀取內(nèi)存中的值為“舊值”,再拿“舊值”與“內(nèi)存中的值”進(jìn)行判斷。直到“舊值”與“內(nèi)存中的值”一樣,就把“新值”更新到內(nèi)存當(dāng)中。

這里要注意上面的CAS操作是分3個(gè)步驟的,但是這3個(gè)步驟必須一次性完成,因?yàn)椴蝗坏脑?,?dāng)判斷“內(nèi)存中的值”與“舊值”相等之后,向內(nèi)存寫入“新值”之間被其他線程修改就可能會(huì)得到錯(cuò)誤的結(jié)果。JDK中的sun.misc.Unsafe中的compareAndSwapInt等一系列方法Native就是用來完成這種操作的。另外還要注意,上面的CAS操作存在一些問題:

  1. 一個(gè)典型的ABA的問題,也就是說當(dāng)內(nèi)存中的值被一個(gè)線程修改了,又改了回去,此時(shí)當(dāng)前線程看到的值與期望的一樣,但實(shí)際上已經(jīng)被其他線程修改過了。想要解決ABA的問題,則可以使用傳統(tǒng)的互斥同步策略。
  2. CAS還有一個(gè)問題就是可能會(huì)自旋時(shí)間過長。因?yàn)镃AS是非阻塞同步的,雖然不會(huì)將線程掛起,但會(huì)自旋(無非就是一個(gè)死循環(huán))進(jìn)行下一次嘗試,如果這里自旋時(shí)間過長對(duì)性能是很大的消耗。
  3. 根據(jù)上面的描述也可以看出,CAS只能保證一個(gè)共享變量的原子性,當(dāng)存在多個(gè)變量的時(shí)候就無法保證。一種解決的方案是將多個(gè)共享變量打包成一個(gè),也就是將它們整體定義成一個(gè)對(duì)象,并用CAS保證這個(gè)整體的原子性,比如AtomicReference。

無同步方案

所謂無同步方案就是不需要同步,比如一些集合屬于不可變集合,那么就沒有必要對(duì)其進(jìn)行同步。有一些方法,它的作用就是一個(gè)函數(shù),這在函數(shù)式編程思想里面比較常見,這種函數(shù)通過輸入就可以預(yù)知輸出,而且參與計(jì)算的變量都是局部變量等,所以也沒必要進(jìn)行同步。還有一種就是線程局部變量,比如ThreadLocal等。

2.3 鎖優(yōu)化

自旋鎖和自適應(yīng)自旋

自旋鎖用來解決互斥同步過程中線程切換的問題,因?yàn)榫€程切換本身是存在一定的開銷的。如果物理機(jī)器有一個(gè)以上的處理器,能讓兩個(gè)或以上的線程同時(shí)并行執(zhí)行,我們就可以讓后面請(qǐng)求鎖的那個(gè)線程“稍等一會(huì)”,但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快就會(huì)釋放鎖。為了讓線程等待,我們只須讓線程執(zhí)行一個(gè)忙循環(huán)(自旋),這項(xiàng)技術(shù)就是所謂的自旋鎖。

自旋鎖在JDK 1.4.2中就已經(jīng)引入,只不過默認(rèn)是關(guān)閉的,可以使用-XX:+UseSpinning參數(shù)來開啟,在JDK 1.6中就已經(jīng)改為默認(rèn)開啟了。自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時(shí)間的, 所以如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之如果鎖被占用的時(shí)間很長,那么自旋的線程只會(huì)白白消耗處理器資源,而不會(huì)做任何有用的工作, 反而會(huì)帶來性能的浪費(fèi)。

我們可以通過參數(shù)-XX:PreBlockSpin來指定自旋的次數(shù),默認(rèn)值是10次。在JDK 1.6中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的時(shí)間不再固定了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長的時(shí)間, 比如100個(gè)循環(huán)。另一方面,如果對(duì)于某個(gè)鎖,自旋很少成功獲得過,那在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋過程,以避免浪費(fèi)處理器資源。

下面是自旋鎖的一種實(shí)現(xiàn)的例子:

public class SpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while(!sign.compareAndSet(null, current)) ;
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }
}
復(fù)制代碼

從上面的例子我們可以看出,自旋鎖是通過CAS操作,通過比較期值是否符合預(yù)期來加鎖和釋放鎖的。在lock方法中如果sign中的值是null,也就代標(biāo)鎖被釋放了,否則鎖被其他線程占用,需要通過循環(huán)來等待。在unlock方法中,通過將sign中的值設(shè)置為null來通知正在等待的線程鎖已經(jīng)被釋放。

鎖粗化

鎖粗化的概念應(yīng)該比較好理解,就是將多次連接在一起的加鎖、解鎖操作合并為一次,將多個(gè)連續(xù)的鎖擴(kuò)展成一個(gè)范圍更大的鎖。

public class StringBufferTest {
    StringBuffer sb = new StringBuffer();

    public void append(){
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}
復(fù)制代碼

這里每次調(diào)用sb.append()方法都需要加鎖和解鎖,如果虛擬機(jī)檢測到有一系列連串的對(duì)同一個(gè)對(duì)象加鎖和解鎖操作,就會(huì)將其合并成一次范圍更大的加鎖和解鎖操作,即在第一次append()方法時(shí)進(jìn)行加鎖,最后一次append()方法結(jié)束后進(jìn)行解鎖。

輕量級(jí)鎖

輕量級(jí)鎖是用來解決重量級(jí)鎖在互斥過程中的性能消耗問題的,所謂的重量級(jí)鎖就是sychronized關(guān)鍵字實(shí)現(xiàn)的鎖。synchronized是通過對(duì)象內(nèi)部的一個(gè)叫做監(jiān)視器鎖(monitor)來實(shí)現(xiàn)的。但是監(jiān)視器鎖本質(zhì)又依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間。

首先,對(duì)象的對(duì)象頭中存在一個(gè)部分叫做Mark word,其中存儲(chǔ)了對(duì)象的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、GC年齡等,其中有2bit用于存儲(chǔ)鎖標(biāo)志位。

在代碼進(jìn)入同步塊的時(shí)候,如果對(duì)象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài)),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝。拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)Φ?code>Mark word。并且將對(duì)象的Mark Word的鎖標(biāo)志位變?yōu)?00",表示該對(duì)象處于鎖定狀態(tài)。更新操作失敗了,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行。否則說明多個(gè)線程競爭鎖,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志的變?yōu)椤?0”,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。 而當(dāng)前線程便嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環(huán)去獲取鎖的過程。

從上面我們可以看出,實(shí)際上當(dāng)一個(gè)線程獲取了一個(gè)對(duì)象的輕量級(jí)鎖之后,對(duì)象的Mark Word會(huì)指向線程的棧幀中的Lock Record,而棧幀中的Lock Record也會(huì)指向?qū)ο蟮?code>Mark Word。 棧幀中的Lock Record用于判斷當(dāng)前線程已經(jīng)持有了哪些對(duì)象的鎖,而對(duì)象的Mark Word用來判斷哪個(gè)線程持有了當(dāng)前對(duì)象的鎖。 當(dāng)一個(gè)線程嘗試去獲取一個(gè)對(duì)象的鎖的時(shí)候,會(huì)先通過鎖標(biāo)志位判斷當(dāng)前對(duì)象是否被加鎖,然后通過CAS操作來判斷當(dāng)前獲取該對(duì)象鎖的線程是否是當(dāng)前線程。

輕量級(jí)鎖不是設(shè)計(jì)用來取代重量級(jí)鎖的,因?yàn)樗思渔i之外還增加了額外的CAS操作,因此在競爭激烈的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。

偏向鎖

一個(gè)對(duì)象剛開始實(shí)例化的時(shí)候,沒有任何線程來訪問它的時(shí)候。它是可偏向的,意味著,它現(xiàn)在認(rèn)為只可能有一個(gè)線程來訪問它,所以當(dāng)?shù)谝粋€(gè)線程來訪問它的時(shí)候,它會(huì)偏向這個(gè)線程。此時(shí),對(duì)象持有偏向鎖,偏向第一個(gè)線程。這個(gè)線程在修改對(duì)象頭成為偏向鎖的時(shí)候使用CAS操作,并將對(duì)象頭中的ThreadID改成自己的ID,之后再次訪問這個(gè)對(duì)象時(shí),只需要對(duì)比ID,不需要再使用CAS在進(jìn)行操作。

一旦有第二個(gè)線程訪問這個(gè)對(duì)象,因?yàn)槠蜴i不會(huì)主動(dòng)釋放,所以第二個(gè)線程可以看到對(duì)象時(shí)偏向狀態(tài),這時(shí)表明在這個(gè)對(duì)象上已經(jīng)存在競爭了,檢查原來持有該對(duì)象鎖的線程是否依然存活,如果掛了,則可以將對(duì)象變?yōu)闊o鎖狀態(tài),然后重新偏向新的線程,如果原來的線程依然存活,則馬上執(zhí)行那個(gè)線程的操作棧,檢查該對(duì)象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級(jí)為輕量級(jí)鎖,(偏向鎖就是這個(gè)時(shí)候升級(jí)為輕量級(jí)鎖的)。如果不存在使用了,則可以將對(duì)象回復(fù)成無鎖狀態(tài),然后重新偏向。

輕量級(jí)鎖認(rèn)為競爭存在,但是競爭的程度很輕,一般兩個(gè)線程對(duì)于同一個(gè)鎖的操作都會(huì)錯(cuò)開,或者說稍微等待一下(自旋),另一個(gè)線程就會(huì)釋放鎖。 但是當(dāng)自旋超過一定的次數(shù),或者一個(gè)線程在持有鎖,一個(gè)在自旋,又有第三個(gè)來訪時(shí),輕量級(jí)鎖膨脹為重量級(jí)鎖,重量級(jí)鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉(zhuǎn)。

如果大多數(shù)情況下鎖總是被多個(gè)不同的線程訪問,那么偏向模式就是多余的,可以通過-XX:-UserBiaseLocking禁止偏向鎖優(yōu)化。

輕量級(jí)鎖和偏向鎖的提出是基于一個(gè)事實(shí),就是大部分情況下獲取一個(gè)對(duì)象鎖的線程都是同一個(gè)線程,它在這種情形下的效率會(huì)比重量級(jí)鎖高,當(dāng)鎖總是被多個(gè)不同的線程訪問它們的效率就不一定比重量級(jí)鎖高。 因此,它們的提出不是用來取代重量級(jí)鎖的,但在一些場景中會(huì)比重量級(jí)鎖效率高,因此我們可以根據(jù)自己應(yīng)用的場景通過虛擬機(jī)參數(shù)來設(shè)置是否啟用它們。

總結(jié)

JMM是Java實(shí)現(xiàn)并發(fā)的理論基礎(chǔ),JMM種規(guī)定了8種操作與8種規(guī)則,并對(duì)voliate、long和double類型做了特別的規(guī)定。

JVM會(huì)對(duì)我們的代碼進(jìn)行重排序以優(yōu)化性能,對(duì)于重排序,JMM又提出了先行發(fā)生原則(happens-before)和as-if-serial語義,以保證程序的最終結(jié)果不會(huì)因?yàn)橹嘏判蚨淖儭?/p>

Java的線程是通過一種輕量級(jí)進(jìn)行映射到內(nèi)核線程實(shí)現(xiàn)的。我們可以使用互斥同步、非阻塞同步和無同步三種方式來保證多線程情況下的線程安全。此外,Java還提供了多種鎖優(yōu)化的策咯來提升多線程情況下的代碼性能。

這里主要介紹JMM的內(nèi)容,所以介紹的并發(fā)相關(guān)內(nèi)容也僅介紹了與JMM相關(guān)的那一部分。但真正去研究并發(fā)和并發(fā)包的內(nèi)容,還有許多的源代碼需要我們?nèi)ラ喿x,僅僅一篇文章的篇幅顯然無法全部覆蓋。

歡迎工作一到五年的Java工程師朋友們加入Java高并發(fā): 957734884,群內(nèi)提供免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用、高并發(fā)、高性能及分布式、Jvm性能調(diào)優(yōu)、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)合理利用自己每一分每一秒的時(shí)間來學(xué)習(xí)提升自己,不要再用"沒有時(shí)間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個(gè)交代!

原文鏈接:https://juejin.im/post/5b4f48e75188251b1b448aa0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,489評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,510評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,866評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,036評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,585評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,331評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,536評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,754評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,273評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,505評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容