一文帶你完全理解同步和鎖的本質(乾貨)

談到鎖,離不開多線程,或者進程間的通訊。 爲了更好地從底層原理去了解鎖的機制,造成體系化的知識,這篇文章我會從進程間通訊底層原理提及,而後介紹一下Java中各類線程通訊的實現機制,最後作一個系統的總結。 html

還記得上次跟你撕逼內存模型的那我的嗎,他又來了,而且向你甩出了一堆問題: java

image-20200225000533712
image-20200225000543895
image-20200225000553812

一、爲何須要通訊

1.一、競態條件

咱們知道,在操做系統中,互相協做的進程之間可能共享一些彼此都能讀寫的公共存儲區,假設兩個進程都須要改寫這個公共的存儲區那麼就會產生競爭關係了。 程序員

下面舉個例子 算法

假設兩個進程a和b共享一個脫機目錄,脫機目錄中有許多槽位,free記錄了下一個空的槽位,進程能夠往下一個空槽位中寫入內容。 數據庫

進程a準備往下一個空槽位寫入內容"test",進程b準備往下一個空槽位寫入內容「good」。 編程

咱們來分析下極端狀況: 後端

image-20200222215815129

能夠發現,因爲發生了時鐘中斷,兩個進程都往槽位3寫入了內容,進程b的內容被進程a的內容覆蓋掉了。 bash

像這種因爲兩個或者多個進程讀寫某些共享數據,最後結果取決於進程運行的精確時序,稱爲競態條件服務器

爲了不這種競態條件的出現,就須要找出存在這種競態條件的程序片斷,經過互斥的手段來阻止多個進程同時讀寫共享的數據。 網絡

1.二、臨界區

對共享內存進行訪問的程序片斷稱爲臨界區

爲了實現互斥而選擇適當的原語是任何操做系統的主要涉及內容之一。 後面咱們會詳細討論各類實現互斥的手段,這些手段也是實現進程通訊或者線程通訊的技術基礎。

仍是以上面的例子來講明,爲了不競態條件的產生,咱們須要把獲取空槽位和往槽位寫內容的程序片斷做爲一個臨界區,任何不一樣的進程,不能夠在同一個時刻進入這個臨界區:

image-20200223095433203

如上圖,進程b試圖在a離開臨界區以前進入臨界區,會進入不了,致使阻塞,一般表現的行爲爲: 進程掛起或者自旋等待。

爲了實現這種臨界區的互斥,須要進程之間可以像對話同樣,確認是否能夠進入臨界區執行代碼,這種對話即進程通訊有不少經典的處理方法,下面咱們就逐個的來介紹。

二、常見的實現進程通訊的手段

2.一、忙等待的互斥(自旋等待)

所謂忙等待,指的是進程本身一直在循環判斷是否能夠獲取到鎖了,這種循環也稱爲自旋下面咱們經過屏蔽中斷鎖變量的介紹,依次引出忙等待的相關互斥手段方法。

2.1.一、屏蔽中斷

以下圖,在進程進入臨界區以前,調用local_irq_disable宏來屏蔽中斷,在進程離開臨界區以後,調用local_irq_disable宏來使能中斷。

image-20200223095140312

CPU只有發生時鐘中斷或其餘中斷纔會進行進程切換,也就是說,屏蔽中斷後,CPU不會切換到其餘進程。 可是,這僅僅對執行disable的那個CPU有效,其餘CPU仍將繼續運行,也就是說多核處理器這種手段無效。

另外,這個屏蔽中斷是用戶進程觸發的,若是用戶進程長時間沒有離開臨界區,那就意味着中斷一直啓用不了,最終致使整個系統的終止。

因而可知,在這個多核CPU普及的時代,屏蔽中斷並非實現互斥的良好手段。

2.1.二、鎖變量

上面一種硬件的解決方案,既然硬件解決不了,那麼咱們嘗試經過軟件層面的解決方案去實現。 咱們添加一個共享鎖變量,變量爲0,則表示可進入臨界區,進入以後,設置爲1,離開臨界區重置爲0,以下圖所示:

image-20200223100855287

可是因爲對Lock的check和set是分爲兩步,並不是原子性的,那麼可能會出現以下狀況:

image-20200223101930928

也就是說在進程a把Lock設置爲1以前,b就進行check和set操做了,也獲取到了Lock=0,致使兩個進程同時進入了臨界區。

這種非原子性的檢查並設置鎖操做仍是會存在競態條件,並不能做爲互斥的解決方案。

接下來咱們升級一下程序,爲了不這種競態條件,咱們讓進程間嚴格輪換的方式去爭搶使用Lock的機會。

2.1.三、嚴格輪換法

所謂嚴格輪換法,就是指定一個標識位turn,當turn=0的時候讓進程a進入臨界區,當turn=1的時候,讓進程b進入臨界區。 如下是實現代碼:

1// 進程a 2while(TRUE){ 3    while(turn != 0);      /* 循環測試turn,看其值什麼時候變爲0 */ 4    critical_region();     /* 進入臨界區 */ 5    turn = 1;              /* 讓給下一個進程處理 */ 6    noncritical_region();  /* 離開臨界區 */ 7} 8// 進程b 9while(TRUE){10    while(turn != 1);      /* 循環測試turn,看其值什麼時候變爲1 */11    critical_region();     /* 進入臨界區 */12    turn = 0;              /* 讓給下一個進程處理 */13    noncritical_region();  /* 離開臨界區 */14}複製代碼

這種方法可能致使在循環中不停的測試turn,這稱爲忙等待,比較浪費CPU,只有有理由認爲等待時間是很是短的情形下,才使用忙等待,用於忙等待的鎖,稱爲自旋鎖(spin lock)。

假設如今進程a在臨界區裏面,而且執行了turn=1,準備把臨界區輪換給進程b,可是這個時候進程b正在處理其餘事情,那麼這個臨界區就一直被進程b阻塞了。 進程a想從新進入也須要等待。

也就是說,我唱完一首歌,把麥給了你,輪到你唱,這個時候你拿着麥去上廁所了。 那麼我想唱歌,也只能等到你上完廁所,唱完歌,把麥的使用權交接給我,我才能夠繼續唱。

你上廁所居然影響到了我唱歌,就是所謂的臨界區外運行的進程阻塞了其餘想進入臨界區的進程。

看來這種解決方案並非一個很好的選擇。

接下來咱們經過一種G.L.Peterson發現的一種互斥算法來實現互斥功能。

2.1.四、Peterson解法

既然Linus說了Talk is cheap. Show me the code. 話很少說,咱們直接上代碼:

1#define FALSE 0 2#define TRUE1 3#define N 2                       /* 進程數量 */ 4 5int turn;                         /* 如今輪到誰? */ 6int interested[N];                /* 全部值初始化爲0 (FALSE) */ 7 8void enter_region(int process){   /* 進程是0或1 */ 9    int other;                    /* 其餘進程號 */10    other = 1 - process;          /* 另外一方進程 */11    interested[process] = TRUE;   /* 表示所感興趣 */12    turn = process;13    while(turn == process && interested[other] == TRUE);/* 空循環 */14}1516void leave_region(int process){17    interested[process] = FALSE;  /* 表示離開臨界區 */18}複製代碼

算法關鍵代碼是while循環,若是併發執行,當進程0調用完enter_region以後,變量值以下:

1interested[0] = TRUE2turn=0複製代碼

進程1調用完enter_region後,給turn賦值=1,覆蓋了進程0的賦值:

1interested[0] = TRUE2interested[1] = TRUE3turn=1複製代碼

而後進程1發現turn == process成立,而且interested[other] == TRUE,而後卡在這裏自旋等待,直到另外一個進程離開了臨界區。

能夠看到,這個算法只適用於兩個進程間的互斥處理,更多進程就沒辦法了。

接下來,咱們經過一種硬件指令的方式,幫助咱們更好的實現互斥。

2.1.五、基於硬件指令

基於硬件指令通常是基於衝突檢測的樂觀併發策略: 先進行操做,若是沒有其餘進程爭用共享數據,操做就成功了,若是產生了衝突,就進行補償,不斷重試。

樂觀併發策略須要硬件指令集的發展才能進行,須要硬件指令實現: 操做+衝突檢的原子性。

這類指令有:

  • 測試並設置鎖 Test and Set Lock  (TSL)

  • 獲取並增長 Fetch-and-Increment

  • 交換 Swap

  • 比較並交換 Compare-and-Swap (CAS)

  • 加載連接/條件存儲 Load-linked / Store-Conditional  LL/SC

2.1.5.一、TSL指令

測試並設置鎖 Test and Set Lock  (TSL),指令格式以下:

TSL RX, LOCK

做用是將一個內存字lock讀到寄存器RX,而後將lock設置爲一個非0值。

執行原理: 執行TSL指令的CPU會鎖住內存總線,禁止其餘CPU在這個指令結束以前訪問內存。

爲了使用TSL指令,須要使用一個共享變量lock來協調多內存的訪問。 lock=0時,任何進程均可以使用TSL指令將其設置爲1,並讀寫共享內存,當操做結束時,進程使用move指令將lock的值從新設置爲0。 下面是實現的關鍵代碼:

1enter_region:2    TSL REGISTER,LOCK      | 複製鎖到寄存器並將鎖設爲13    CMP REGISTER,#0        | 鎖是0嗎?4    JNE enter_region       | 若不是0,說明鎖已被設置,因此循環5    RET                    | 返回調用者,進入臨界區67leave_region:8    MOVE LOCK,#0           | 在鎖中存入 09    RET                    | 返回調用者複製代碼
image-20200223133446606

若是TSL原子操做沒有成功,則從新跳轉到enter_region方法循環執行。 這個跟Peterson算法有點相似,不過TSL能夠支持任意多個進程的併發執行。 **

2.1.5.二、CAS指令

IA64 和 X86 使用cmpxchg指令完成CAS功能。

cas 內存位置 舊預期值 新值

CAS存在ABA問題,可使用版本號進行控制,保證其正確性。

JDK中的CAS,相關類: Unsafe裏面的compareAndSwapInt()以及compareAndSwapLong()等幾個方法包裝提供。 只有啓動類加載器加載的class才能訪問他,或者經過反射獲取。

硬件指令既能夠實現忙等互斥,也能夠實現進程掛起阻塞,關鍵看具體的實現代碼,這裏使用了JNE指令進行跳轉循環等待,後面咱們會介紹用TSL指令實現進程掛起阻塞的互斥量。

2.二、睡眠與喚醒(進程掛起)

以上的解法都是能夠實現互斥的,可是存在忙等,致使浪費CPU時間的問題,若是同步資源鎖定時間很短,那麼這個等待仍是值得的,可是若是鎖佔用時間過長,那麼自旋就會浪費CPU資源了。 另外可能會致使優先級反轉問題

以下圖,進程H優先級較高,進程L先進入了臨界區,而後H變到就緒狀態,準備運行,如今H開始忙等待。 H就緒是L不會被調度到,不會離開臨界區,因此H會永遠等待下去:

image-20200223133911701

爲了避免浪費CPU資源,咱們可使用進程間通訊的原語sleepwakeupsleep形成調用者阻塞,直到其餘進程喚醒它。 下面咱們根據經典的生產者消費者的問題,引出信號量以及阻塞的概念。

2.2.一、生產者消費者問題

以下圖,生產者往隊列裏面生產消息,消費者從隊列裏面取消息進行消費:

image-20200223143442359

當消息隊列滿的時候,生產者準備進行睡眠,但還沒睡着:

image-20200223144249182

消費者消費了一條消息以後,他認爲生產者正在睡覺,準備通知生產者也起牀幹活生產消息了:

image-20200223144606951

但是這個時候,生產者實際上都還沒真正睡着,因此: wakeup信號丟失了!!!

wakeup信號丟失以後,生產者才真正的睡着了,這個時候消費者殊不知道生產者睡着了,因而一直在消費消息,知道消息消費完了,消費者本身也睡覺了。 最後兩個進程都睡着了,世界清靜了:

image-20200223145447834

生產者消費者完整代碼以下:

1#define N 100                             /* 緩衝區中的槽數量 */ 2int count = 0;                            /* 緩衝區中的數據項數目 */ 3 4// 生產者 5void producer(void){ 6    int item; 7 8    while(TRUE){                          /* 無限循環 */ 9        item = produce_item()             /* 產生下一新數據項 */10        if(count == N) sleep();           /* 若是緩衝區滿了,就進入休眠狀態 */11        insert_item(item);                /* 將新數據放入緩衝區中 */12        count = count + 1;                /* 緩衝區數據項計數器+1 */13        if(count == 1) wakeup(consumer);  /* 緩衝區不爲空則喚醒消費 */14    }15}1617// 消費者18void consumer(void){19    int item;2021    while(TRUE){                          /* 無限循環 */22        if(count == 0) sleep();           /* 若是緩衝區是空的,則進入休眠 */23        item = remove_item();             /* 從緩衝區中取出一個數據項 */24        count = count - 1                 /* 將緩衝區的數據項計數器-1 */25        if(count == N - 1) wakeup(producer); /* 緩衝區不滿,則喚醒生產者? */26        consumer_item(item);              /* 打印數據項 */27    }28}複製代碼

怎麼解決這種進程之間不一樣步,致使的死鎖問題呢,接下來咱們就經過信號量來實現。

2.2.二、信號量

經過使用一個整型變量來累計喚醒次數,以供以後使用。 這個變量就是信號量。 能夠經過如下方式操做這個變量:

  • down能夠用sleep來表示,若是此刻信號量大於0,則將其值-1,若是=0,則進入進程進行睡眠;

  • up能夠用wakeup來表示,up操做會使信號量+1,若是由於多個進程睡眠而沒法完成先去的down操做,系統會選擇一個進程喚醒並完成down操做,但信號量值還是0,取而代之的是睡眠進程數量減1;

檢查數值、修改變量值以及可能發生的睡眠或者喚起操做是原子性的。

信號量原理:

檢查數值、修改變量值以及可能發生的休眠或者喚起操做是原子性的,一般將up和down做爲系統調用來實現;

當執行如下操做時,操做系統暫時屏蔽所有中斷: 檢查信號量、更新、可能發生的休眠或者喚醒,這些操做須要不多的指令,所以中斷不會形成影響;

若是是多核CPU,信號量同時會被保護起來,經過使用TSL或者XCHG指令確保同一個時刻只有一個CPU對信號量進行操做。

使用信號量解決進程同步問題代碼以下:

1#define N 100                   /* 緩衝區中的槽數目 */ 2typedef int semaphore;          /* 信號量是一種特殊的整型數據 */ 3semaphore mutex = 1;            /* 控制對臨界區的訪問 */ 4semaphore empty = N;            /* 計數緩衝區的空槽數目 */ 5semaphore full = 0;             /* 計數緩衝區的滿槽數目 */ 6 7void producer(void){  8 9  int item;  1011  while(TRUE){                  /* TRUE是常量1 */12    item = producer_item();     /* 產生放在緩衝區中的一些數據 */13    down(&empty);               /* 將空槽數目-1  */14    down(&mutex);               /* 進入臨界區  */15    insert_item(item);          /* 將新數據放入緩衝區中 */16    up(&mutex);                 /* 離開臨界區 */17    up(&full);                  /* 將滿槽數目+1 */18  }19}2021void consumer(void){2223  int item;2425  while(TRUE){                  /* 無限循環 */26    down(&full);                /* 將滿槽數目-1 */27    down(&mutex);               /* 進入臨界區 */28    item = remove_item();       /* 從緩衝區取出數據項 */29    up(&mutex);                 /* 離開臨界區 */30    up(&empty);                 /* 將空槽數目+1 */31    consume_item(item);         /* 處理數據項 */32  }33}複製代碼

如上,每一個進程在進入關鍵區域以前執行down操做,在離開關鍵區域以後執行up操做,這樣就能夠確保互斥了。 上面的代碼其實是經過兩種不一樣的方式來使用信號量:

  • mutex用於互斥,確保任意時刻只有一個進程可以對緩衝區相關變量進行讀寫,互斥是用於避免進程混亂鎖必須的一種操做;

  • fullempty經過計數,確保事件的發生或者不發生,這兩個信號量的用意與mutex不一樣。

2.2.三、互斥量

若是僅僅是須要互斥,而不是計數能力,可使用信號量的簡單版本: mutex 互斥量。 通常用整型表示,經過調用:

  • mutex_lock進行加鎖,若是加鎖時處於解鎖狀態(0表示解鎖,其餘值表示加鎖,比1大的值表示加鎖的次數),則調用成功;

  • mutex_unlock進行解鎖。

互斥量能夠經過TSL或者XCHG指令實現,下面是用戶線程包的mutex_lockmutex_unlock的代碼:

1mutex_lock: 2    TSL REGISTER,MUTEX    | 將互斥信號量複製到寄存器,而且將互斥信號量置爲1 3    CMP REGISTER,#0       | 互斥信號量是0嗎? 4    JZE ok                | 若是互斥信號量爲0,它被解鎖,因此返回 5    CALL thread_yield     | 互斥信號量忙;調度其餘線程 6    JMP mutex_lock        | 稍後再試 7ok:  RET                  | 返回調用者,進入臨界區 8 9mutex_unlock:10    MOVE MUTEX,#0         | 將mutex置爲 011    RET                   | 返回調用者複製代碼

以上代碼和enter_region的區別?

  • enter_region失敗的時候會始終重試,而這裏會調度其餘進程進行執行,這樣早晚擁有鎖的進程會進入運行並釋放鎖;

  • 在用戶線程中,enter_region經過忙等待試圖獲取鎖,將永遠循環下去,絕對不會獲得所,由於其餘線程不能獲得運行進行釋放鎖。 沒有時鐘中止運行時間過長的線程。

線程庫沒法像進程那樣經過時鐘中斷強制線程讓出CPU。 在單核系統中若是一個線程霸佔了CPU,那麼該進程中的其餘線程就沒法執行了。

因爲thread_yield僅僅是一個用戶空間的進程調度,因此它運行很是快捷。 這樣mutex_lockmutex_unlock都不須要任何內核調用,從而實現了在用戶空間中的同步,這個過程僅僅須要少許的同步。

有些線程包也會提供mutex_trylock,嘗試獲取鎖或者失敗,讓調用方本身決定是等待下去仍是使用個替代方法。

2.三、管程

爲了可以編寫更加準確無誤的程序,因而出現了管程(monitor)的概念:

管程是程序、變量和數據結構等組成的集合,構成一個特殊模塊或者軟件包,進程能夠調用管程中的程序,可是不能在管程以外聲明的過程當中直接訪問管程內的數據結構

注意: 管程是語言概念而C語言不支持它。 下面是pascal語言描述的管程:

1monitor example 2    integer i; 3    condition c; 4 5    procedure producer(); 6    . 7    end; 8 9    procedure consumer();10    .11    end;12end monitor;複製代碼

管程中任意時刻只能有一個活躍的進程,從而實現了互斥。 特定語言的編譯器知道如何編譯管程,翻譯爲底層的機器碼: 當進程調用管程中的程序的時候,改程序的前面幾條指令會檢查管程中是否有其餘活躍的進程,若是有,則掛起當前調用。

管程的互斥由編譯器負責決定。 通用作法是使用互斥量二進制信號量

Java中的synchronized關鍵字正是基於管程實現的,咱們後面會具體介紹。

經過臨界區的自動互斥,管程比信號量更容易保證並行編程的正確性可是管程是編程語言的概念,須要編譯器識別並用某種方式對互斥作出保證,C語言就沒有管程,因此不能依賴編譯器來遵照互斥規則。

2.四、消息傳遞

不管是經過硬件指令,仍是信號量阻塞,或者管程,都是設計用來解決一個或者多個CPU上的互斥問題的。 可是在分佈式系統中農,不一樣服務器的CPU有本身的私有內存,經過網絡相連,這些原語就會失效了。 因此須要其餘方式來實現進程間的通訊,最多見的就是消息傳遞

2.五、屏障

這個是專門給進程組而不是進程間實現同步的。

有寫程序中劃分了若干階段,而且規定,除非全部進程都準備就緒着手下一個階段,不然任何進程都不能進入下一個階段,能夠在每一個階段結尾安裝屏障來實現這種行爲,當一個進程達到屏障的時候,就會被屏障阻攔,直到全部進程都達到該屏障爲止。

第2節內容主要提煉於《現代操做系統》一書,並添加了一些輔助理解的圖片,給枯燥的描述添加一點趣味兒。

三、常見的Java線程實現通訊的手段和原理

線程通訊是創建在線程模型之上的,咱們首先來說一下Java的線程模型。

關於Java線程採用的線程模型

注意: 上面說的互斥量實現,是用戶線程包中的實現,因此不須要內核調用。 而Java中的互斥量會有所不一樣,仍是須要進行系統調用,由用戶態切換到內核態。 JVM規範未指定Java線程須要用哪一種線程模型。

常見線程實現方式有如下幾種:

使用內核線程實現

內核線程(KLT, Kernel-Level Thread)是直接由操做系統內核支持的線程,內核完成線程切換,內核經過操縱調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。

各類線程操做,如建立、析構及同步,都須要進行系統調用,須要在用戶態內核態中來回切換。

程序經過內核線程的高級接口: 輕量級進程(LWP, Light Weight Process)操做內核線程。 他們之間的關係以下:

image-20200223231207126

每一個輕量級進程都須要有一個內核線程的支持,所以輕量級進程要消耗必定的內核資源如內核線程的棧空間,因此一個系統可以支持的輕量級進程的數量是有限的。

內核線程至關於內核的分身,這樣能夠內核同時處理多件事情,支持多線程的內核叫多線程內核。

使用用戶線程實現

咱們上面將的互斥量的實現,即便基於用戶線程的。 用戶線程創建在用戶控件的線程庫上,系統內核感知不到線程的實現。 線程的建立、同步、銷燬和調度都是在用戶態中進行,無需內核幫助。

缺點: 沒有內核的幫忙,線程操做,線程阻塞同步處理起來是很麻煩的事情。

image-20200223232011459

使用用戶線程+輕量級進程混合實現

image-20200223232336679

Java線程的實現

JVM規範並無限定Java線程須要那種模型,對於Windows和Linux版本使用的是1:1的線程,映射到輕量級進程中。

3.一、synchronized關鍵字

經過使用synchronized,能夠實現任意大語句塊的原子性單位,使咱們可以解決volatile沒法實現的read-modify-write問題。 底層命令能夠參考我這篇文章: 2.三、同步操做Synchronized

Java中的synchronized關鍵字也是經過管程實現的,保證了操做單原子性和可見性。 在同一類中的synchronized方法,同一時刻只能有一個線程在調用。

3.1.一、synchronized鎖特色

3.1.1.一、鎖升級

管程通用作法是經過互斥量實現的,這會致使線程掛起阻塞,這種傳統的鎖稱爲重量級鎖在JDK1.6以後引入了輕量級鎖偏向鎖的概念。 爲此,存在一個鎖升級的過程。

對象頭中記錄了對象的鎖類型,咱們再來回顧一下對象的內存佈局:

image-20191201231334798
image-20191201231334798

咱們先來介紹下輕量級鎖和偏向鎖。

輕量級鎖

之因此叫輕量級鎖,是與互斥量致使線程掛起阻塞這種重量級鎖對比的叫法,沒錯,我就是比互斥重量級鎖輕巧多了。

從未鎖定到輕量級鎖定的過程仍是有點繁瑣的,涉及複製Mark WordCAS指定鎖記錄,指定失敗的狀況下可能還須要膨脹爲重量級鎖在釋放鎖的時候會CAS替換Mark Word,替換失敗則說明有其餘線程在等待獲取鎖,這個時候在釋放鎖的同時須要喚起其餘線程。 我整理了一個圖,方便更加直觀的觀察到這個過程Mark Word的變化:

image-20200223215127067
image-20200223215127067
偏向鎖

所謂偏向鎖,就是在數據無競爭的狀況下,消除同步原語,進一步提升運行性能。

輕量級鎖在無競爭狀況下使用CAS消除同步使用的互斥量偏向鎖在無競爭的狀況把整個同步都消除了,更加輕量級。

爲何叫偏向鎖,覺得偏愛呀,總是偏袒第一個獲取到他的線程若是接下來的確沒有其餘線程競爭,那持有偏向鎖的線程就永遠不須要再進行同步了。

若是開啓了偏向鎖(JDK1.8默認是開啓的),那麼當鎖對象第一次被線程獲取的時候,虛擬機就會嘗試設置爲偏向鎖模式:

image-20200223214657099
image-20200223214657099

一旦有其餘線程競爭,那麼偏向模式就結束了。 變爲未鎖定或者輕量級鎖的狀態。 他們之間的升級關係以下圖所示:

image-20200223233032942
image-20200223233032942

3.1.1.二、鎖優化

爲了提升synchronized的性能,HotSpot虛擬機團隊在JDK 1.6版本花費了大量精力進行鎖優化,包括:

  • 自旋鎖爲了不互斥量頻繁進行內核態切換帶來的壓力,引入了自旋鎖。 默認會自旋10次試圖獲取鎖,可使用參數設置: -XX:PreBlockSpin

  • 自適應自旋鎖若是一個鎖自旋不多成功,那麼獲取這個鎖可能會去掉自旋階段; 若是自旋獲取成功機率比較高,那麼運行自旋等待持續時間相對更長;

  • 鎖消除這是即時編譯器乾的活,通常經過逃逸分析的數據支持進行鎖消除,通常程序員都不會直接在單線程代碼中顯示的使用鎖,可是有時候雖然只有一行代碼:

  • str = "a" + "b" + "."

  • 可是在JDK5以前底層是翻譯爲了StringBuffer的append()操做,該方法是包含synchronized鎖的,因此這種狀況及時編譯器仍是會進行鎖消除。

  • 鎖粗化若是一些列連續的鎖操做都是反覆對同一個對象的加鎖和解鎖,並無線程競爭,那麼這個時候爲了優化性能,會擴大鎖的範圍。

固然,上面說起的鎖升級,也是鎖優化的一種手段。

3.1.1.三、可重入

對於同一個鎖,若是一個線程成功進入了臨界區,那麼該線程在持有鎖的同時,能夠反覆進入該鎖。 synchronized鎖的對象頭markword會記錄該鎖的線程持有者和進入鎖的次數的計數器。

每退出一個synchronized方法塊,計數器就-1,直到0的時候就釋放鎖。

3.1.1.四、悲觀鎖

爲何說它是一把悲觀鎖呢,由於假設有一個線程獲取到了鎖,那麼其餘嘗試獲取鎖的線程只能等待,因而悲觀的去睡覺了,等到別人叫醒以後才從新去競爭獲取鎖。

3.1.二、synchronized鎖使用場景

3.二、理清各類鎖的分類

咱們在各類文章書籍裏面可能會看到對鎖的各類分類,都是什麼意思呢? 如今咱們就經過簡短的描述來解釋下,讓你們有個形象的認識。

3.2.一、樂觀鎖和悲觀鎖

樂觀鎖

樂觀鎖老是很樂觀的認爲不會有太多人會搶佔鎖,因此通常不會先進行加鎖,等到出了問題以後再處理。 典型的實現如CAS。

對於樂觀鎖,可能若是發現的確出現了問題,通常會經過自旋,或者直接放棄等的方式進行處理。

鎖以樂觀鎖適用於併發寫入少,大部分是讀的場景。 這樣就能夠提升加快自旋的成功率了。

悲觀鎖

悲觀鎖就是很悲觀的認爲會有不少人想佔用這個鎖,悲觀鎖爲了保證本身能夠拿到鎖,一上來就嘗試鎖定,若是鎖不住,那就放棄了,直接睡覺去了,也就是線程掛起,等到下次有人叫他起牀的時候,纔會從新參與到鎖的競爭中來。

對於競爭比較激烈,臨界區消耗比較多的時間的場景,比較適合悲觀鎖。 無論臨界區消耗多長的時間,也不會加大互斥鎖的開銷。

3.2.二、自旋鎖和阻塞鎖

自旋鎖

這個概念,詳細看完上文的你應該比較瞭解了,就是獲取鎖失敗以後,循環重試。

阻塞鎖

這個概念,詳細看完上文的你應該比較瞭解了,就是獲取鎖失敗以後,掛起線程。

3.2.三、共享鎖和排他鎖

共享鎖

又稱讀鎖,既然共享了,那麼就不能隨便刪除和修改了。 否則人家好端端的看着你up給他的Java教學視頻,忽然下一秒變成了動畫片,叫人家怎麼能接受呢?

排它鎖

跟共享鎖不同,排他鎖就是一旦獲取到了他以後,其餘線程就不再能獲取到了。 既然別人獲取不到了,那麼獲取到排他鎖的我就能夠隨意的進行修改內容了。

3.2.四、可重入鎖

可重入鎖

這個比較容易理解,咱們在上面講synchronized的時候已經介紹了。

可重入鎖在反覆屢次使用同一個鎖的場景下,避免了死鎖的發生。

3.2.五、公平鎖和非公平鎖

公平鎖

公平鎖,就是徹底按照請求順序來分配的鎖,保證了對全部線程公平。

非公平鎖

跟公平鎖不同,是不徹底按照請求順序來處理的。

Java併發包中的ReentrantLock鎖就提供了非公平鎖和公平鎖的實現。

爲何要有公平鎖呢? 設想一下,咱們到銀行辦理業務取票了,若是無論發生什麼狀況,徹底按照順序來辦理,這就是公平鎖。

可是輪到一個號以後,假如那個號的人恰好去外面買東西了,若是你們要繼續等它回來辦理,就會很花時間,因而索性讓人去搶櫃檯窗口,先搶到的人就先辦理,若是連續兩次都搶位失敗了,那麼咱們就把這我的放入排隊隊列,等到搶到窗口的人辦理完了業務,再輪流叫喚他們,這就是非公平鎖。

可是若是窗口的那我的辦理業務的時間好久,忽然叫一大波人衝上來搶窗口,是搶不到的呀,也就是說對於業務執行時間很長的場景,非公平鎖其實效率並不高。

很明顯,公平鎖吞吐量小,但能夠保證每一個線程在等一段時間總有機會執行;

而非公平鎖吞吐量更大,可是可能有些線程會長時間得不到執行。

3.2.六、可中斷鎖和不可中斷鎖

可中斷鎖

能夠響應線程中斷的鎖,如ReentrantLock.lockInterruptibly()。

不可中斷鎖

不能夠響應線程中斷的鎖,如ReentrantLock.lock()。


其實Java併發包中針對不一樣的使用場景,也提供了不少的鎖,咱們能夠直接拿來用。 關於其實現思路,因爲篇幅所限,我打算後面單獨放到一篇文章中講解,包括實現原理,使用場景等。 相信有了本文這些底層知識以後,在看其餘頂層的實現,都會駕輕就熟點。

四、結語

好了,咱們今天就講到這裏了,可以看到這裏的朋友們是真的很熱愛技術,想對大家說: 加油,你們都是最棒的!

本文爲arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。

你們能夠關注個人博客: itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。

若是您以爲讀完本文有所收穫的話,能夠關注個人帳號,或者點個贊碼字不易,你的支持很重要。

關注個人公衆號,及時獲取最新的文章。

References

《現代操做系統》

《深刻理解Java虛擬機: JVM高級特性與最佳實踐》

聊聊 Java 的幾把 JVM 級鎖

Java併發編程—細說J.U.C下Lock的分類及特色詳解(結合案例和源碼)


本文做者: arthinking

博客連接: https://www.itzhai.com/cpj/process-synchronization-and-lock.html

一文帶你完全理解同步和鎖的本質(乾貨)

版權聲明: BY-NC-SA許可協議: 創做不易,如需轉載,請務必附加上博客連接,謝謝!


相關文章
相關標籤/搜索