java鎖的底層原理

知識整理

  1. Synchronized 內置鎖,JVM級別
    1. 使用
    2. 底層 鎖升級過程、CAS操作的缺點【替換線程和copy mw】
    3. 優化
      1. 代碼優化:同步代碼塊、減少鎖粒度、讀鎖併發
      2. JDK自帶 偏置鎖、輕量級鎖(CAS操作)、自適應自旋、鎖粗化、鎖消除
  2. Volatile
      1. 概念:非阻塞可見性、禁止指令重排序*
      2. 與syn區別: 無法實現原子操作、使用場景--單線程、不依賴當前值
  3. Reentrantlock 顯示鎖:基於AQS實現,API級別
    1. AQS原理:
      1. 數據結構:state、waitstate【signal-1、傳播-3】、
      2. 獨佔、共享 tryAcquireShared
    2. 非公平鎖
    3. 特性鎖 可重入、輪詢、定時、可中斷
    4. 優點、使用場景
    5. 與Syn區別、Syn優點
  4. 死鎖
    1. 概念:多個線程因競爭資源而互相等待的僵局;4個必要條件:資源互斥、不可剝奪、保持與請求、循環等待
    2. 死鎖避免:鎖順序、鎖時限、死鎖檢測與恢復
    3. 死鎖檢測與恢復:分配資源時不加條件;檢測時機:進程等待、定時、利用率下降
      1. 檢測算法:資源分配表、遍歷鎖關係圖
      2. 撤銷進程、設置線程隨機優先級
  5. 鎖模式
    1. 讀鎖、寫鎖
    2. 樂觀鎖:用戶解決---數據版本id、時間戳;CAS;適合寫操作少的場景;MVCC實現
    3. 悲觀鎖:數據庫行鎖、頁鎖...

 

synchronized的4種應用方式 jvm內部實現 稱爲:內置鎖

synchronized關鍵字最主要有以下3種應用方式,都是作用在對象上

  1. 修飾類,作用範圍:synchronized括號內, 作用對象:類的所有對象;synchronized(Service.class){ }
  2. 修改靜態方法,作用範圍:整個靜態方法, 作用對象:類的所有對象;
  3. 修飾方法,被修飾的同步方法,作用範圍:整個方法, 作用對象:調用這個方法的對象;
    1. 缺點:A線程執行一個長時間任務,B線程必須等待
  4. 修飾代碼塊,被修飾的代碼塊同步語句塊,作用範圍:大括號內的代碼, 作用對象:調用這個代碼塊的對象;
    1. 優點:減少鎖範圍,耗時的代碼放外面,可以異步調用

 

notify 方法實現只喚醒一個線程,由操作

lock.notify()方法最終通過ObjectMonitor的void notify(TRAPS)實現:

1、如果當前_WaitSet【線程等待的集合】爲空,即沒有正在等待的線程,則直接返回;

2、通過ObjectMonitor::DequeueWaiter 出隊方法,獲取_WaitSet列表中的第一個ObjectWaiter節點,實現也很簡單【選擇哪個線程取決於操作系統對多線程管理的實現】

3、根據不同的策略,將取出來的ObjectWaiter節點,加入到Contention List,或自旋操作,CAS改變第一個節點的的指針爲新增節點

 

notifyAll方法實現

lock.notifyAll()方法最終通過ObjectMonitor的void notifyAll(TRAPS)實現:

通過for循環取出_WaitSet的ObjectWaiter節點,並根據不同策略,加入到_EntryList或則進行自旋操作。

從JVM的方法實現中,可以發現:notify和notifyAll並不會釋放所佔有的ObjectMonitor對象,其實真正釋放ObjectMonitor對象的時間點是在執行monitorexit指令,

一旦釋放ObjectMonitor對象了,Entryset中ObjectWaiter節點所保存的線程,就可以開始競爭ObjectMonitor對象進行加鎖操作了,和ready線程競爭?

 

2.鎖的的實現:內存機制 copy到工作內存->   修改->   刷新主存   的過程,纔會釋放它得到的鎖,達到線程安全。

  1. 鎖住(lock)
  2. 主->從 read load 將需要的數據從主內存拷貝到自己的工作內存(read and load)
  3. 修改 use assign 根據程序流程讀取或者修改相應變量值(use and assign)
  4. 從->主 store write將自己工作內存中修改了值的變量拷貝回主內存(store and write)
  5. 釋放對象鎖(unlock)

線程安全:

  1. 當多個線程訪問某個類,其始終能表現出正確的行爲
  2. 採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,限制其他線程訪問,直到鎖釋放

 

Java中的鎖優化 代碼方式、JDK自帶方式

1.代碼 鎖優化

  1. 減少鎖持有時間 
    1. 使用同步代碼塊,而非同步方法;
  2. 減小鎖粒度
    1. JDK1.6中 ConcurrentHashMap採取對segment加鎖而不是整個map加鎖,提高併發性;
  3. 鎖分離  讀鎖之間不互斥;讀寫分離
    1. 根據同步操作的性質,把鎖劃分爲的讀鎖和寫鎖,讀鎖之間不互斥,提高了併發性

2.JDK1.6 鎖優化 synchronized底層

1.引入偏向鎖、輕量級鎖

  1. 鎖主要存在四中狀態,依次是:無鎖狀態01、偏向鎖狀態01、輕量級鎖狀態00、重量級鎖狀態10,
  2. 會隨着競爭的激烈而逐漸升級,鎖可以升級不可降級,提高 獲得鎖和釋放鎖 效率
  3. “輕量級鎖”和“偏向鎖”作用:減少 獲得鎖和釋放鎖 的性能消耗

優點

缺點

適用場景

偏向鎖

記錄線程iD,若該線程,則不加鎖;鎖狀態01

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

Mark Word複製到鎖記錄,CAS更新指針及標誌位00

自旋方式競爭,競爭的線程不會阻塞,提高了程序的響應速度

如果始終得不到鎖競爭的線程使用,自旋會消耗CPU。

追求響應時間。

同步塊執行速度非常快。

重量級鎖

CAS失敗時,升級。鎖狀態:10

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

偏向鎖獲取過程

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

輕量級鎖

  1. 虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝【因爲棧幀爲線程私有,對象大家都有】
  2. 拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中;
  3. 拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word中的,更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。如果更新成功,則執行步驟4,否則執行步驟5。
  4. 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示。

 

2.鎖粗化 

  1. 如果一系列的連續操作都對同一個對象反覆加鎖和解鎖,如循環體內,很耗性能
  2. 加鎖同步的範圍擴展到整個操作序列的外部:第一個append到最後一個append;不對每個append加鎖

3.鎖消除 逃逸分析的數據的支持

        編譯器判斷到一段代碼中,堆上的數據不會逃逸出當前線程,可以認爲是線程安全的,不必加鎖

4.自旋與自適應自旋:想要獲取鎖的線程做幾個空循環 10 CAS實現

.爲什麼引入:

輕量級鎖失敗後,線程會在操作系統層面掛起

操作系統實現線程之間的切換時,需要從用戶態轉換到核心態,狀態轉換耗時

.解決方法:

當線程在獲取輕量級鎖時CAS操作失敗時,通過自旋讓線程等待,避免線程切換的開銷

假設不久當前的線程可以獲得鎖,虛擬機會讓當前想要獲取鎖的線程做幾個空循環,可能是50個循環或100循環

結果:

如果得到鎖,就順利進入臨界區;如果不能,就將線程在操作系統層面掛起,升級爲重量級鎖

自旋鎖的優化:自適應自旋

自旋是需要消耗CPU的,如果一直獲取不到鎖,線程一直自旋,浪費CPU資源

線程如果自旋成功了,下次自旋的次數會更多,自旋失敗了,自旋的次數就會減少。

 

CAS底層實現原理    用於更新數據

CAS:Compare and Swap, 翻譯成比較並交換 

CAS需要在:操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,最終都會返回內存地址,且是原子操作

需要3個操作數:內存地址V,舊預期值A、新值B

當且僅當V符合預期值A時(即V存儲的值無變化),用B更新A,否則不執行更新,最終都返回內存地址V

適用場景:

  1. 適合資源競爭較少的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;
  2. CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋機率較少,可以獲得更高的性能
  3. synchronized在jdk1.6之後,已經改進優化。synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程衝突較少的情況下,可以獲得和CAS類似的性能;而線程衝突嚴重的情況下,性能遠高於CAS
  4. 原子類中都使用到了CAS

 

volatile詳解

非可見性:

編譯器爲了加快程序運行的速度,對一些變量的寫操作會先在(工作內存)寄存器或者是CPU緩存上進行,最後才寫入內存,這個過程中,變量的新值對其他線程是不可見的

1.實現對所有線程的可見性 SMP:對稱多處理器架構通過總線BUS進行 Cache一致性流量 通信

  1. volatile保證新值立即同步到主存,
  2. 線程對變量讀取的時候,要從主內存中讀,而不是緩存
  3. 變量的賦值一旦變化就會通知到其他線程,如果其他線程的工作內存中存在這個同一個變量拷貝副本,那麼其他線程會放棄這個副本中變量的值,重新去主內存中獲取

適用場景:

確保只有單一的線程修改變量的值 或 運算結果不依賴當前變量值(i++時,運算結果依賴當前變量,且是多個線程改變)

變量不需要與其他的狀態變量共同參與不變約束

2.實現 禁止指令重排序優化 應用:雙邊檢查單例

  1. 爲了減少CPU空閒時間,java不能保證程序執行的順序與代碼中一致,
  2. volatile修飾的變量相當於生成內存屏障,重排序時不能把後面的指令排到屏障之前;指令屏障
  3. 作用:爲了保證happen-before原則:寫、鎖lock、傳遞性、線程啓動、中斷、終結、對象創建的先後關係
  1. 定義了線程、鎖、volatile變量、對象創建的先後關係
  2. 若滿足,則保證一個操作執行的結果需要對另一個操作可見
  3. 判斷數據是否存在競爭、線程是否安全的依據

內存屏障實現方式;volatile的內存語義

編譯器在生成字節碼時,會在指令序列中,插入內存屏障來禁止特定類型的處理器重排序;

寫前後、讀後後;

寫寫【寫上下】寫讀、【讀後面】讀讀、讀寫

 

1.volatile寫操作的前面插入一個StoreStore屏障:禁止上面的寫

2.volatile寫操作的後面插入一個SotreLoad屏障:禁止下面的讀

3.volatile讀操作的後面插入一個LoadLoad屏障:禁止下面的讀

4.volatile讀操作的後面插入一個LoadStore屏障:禁止下面的寫

 

 

3.無法實現 i++ 原子操作

A讀取 i 後,B也讀取 i ,此時A進行 +1,B的 i 就變了

原子類如何解決:CAS,B在進行+1時,檢查此時的 i 跟主存的 i 是否一致,一致才+1;原子類

synchronized和volatile區別 鎖的目標:關注互斥性和可見性

1.鎖提供了兩種主要特性:互斥性(mutual exclusion) 和可見性(visibility)。

  互斥即一次只允許一個線程持有某個鎖,使用該共享數據。

  可見性確保新值立即同步到主存,每次使用前立即從主內存刷新

2.概念

synchronized同步阻塞:釋放鎖之前會將對變量的修改刷新到主存當中;

volatile關鍵字非阻塞:確保新值立即同步到主存,其他線程每次使用前立即從主內存刷新;

3.區別

1)volatile非阻塞,synchronized只有當前線程可以訪問修飾的變量,其他線程阻塞

2)volatile僅能修飾變量,synchronized則可以使用在變量,方法.

3)volatile僅能實現變量的修改可見性,而synchronized則可以保證變量的修改可見性和原子性(操作不可分割)

 

可重入鎖 Re entrantlock

顯示鎖:基於JDK API、AQS、樂觀鎖實現、需要顯式的加鎖以及釋放鎖

  1. 無阻塞的同步機制(非公平鎖實現)
  2. 可實現輪詢鎖、定時鎖、可中斷鎖特性;
  3. 提供了一個Condition(條件)類,對鎖進行更精確的控制
  4. 默認使用非公平鎖,可插隊跳過對線程隊列的處理(因此被稱爲可重入)
    1. ReentrantLock的內部類Sync繼承了AQS,分爲公平鎖FairSync和非公平鎖NonfairSync。
    2. 公平鎖:線程獲取鎖的順序和調用lock的順序一樣,FIFO;喚醒鎖的時間CPU浪費;是否是AQS隊列中的頭結點
    3. 非公平鎖:線程獲取鎖的順序和調用lock的順序無關,先執行lock方法的鎖不一定先獲得鎖
  5. 加鎖和解鎖都需要顯式寫出,實現了Lock接口,注意一定要在適當時候unlock
  6. 總結:公平鎖與非公平鎖對比
  • FairSync:lock()少了插隊部分(即少了CAS嘗試將state從0設爲1,進而獲得鎖的過程)
  • FairSync:tryAcquire(int acquires)多了需要判斷當前線程是否在等待隊列首部的邏輯(實際上就是少了再次插隊的過程,但是CAS獲取還是有的)。

公平鎖的核心

  1. 獲取一次鎖數量,state值
    1. 如果鎖數量爲0,如果當前線程是等待隊列中的頭節點,基於CAS嘗試將state(鎖數量)從0設置爲1一次,如果設置成功,設置當前線程爲獨佔鎖的線程;
    2. 如果鎖數量不爲0或者當前線程不是等待隊列中的頭節點或者上邊的嘗試又失敗了,查看當前線程是不是已經是獨佔鎖的線程了,如果是,則將當前的鎖數量+1;如果不是,則將該線程封裝在一個Node內,並加入到等待隊列中去。等待被其前一個線程節點喚醒。

非公平鎖 兩者都是非公平鎖

  1. 非公平鎖,可以直接插隊獲取鎖,跳過了對隊列的處理,速度會更快
    1. 公平鎖爲了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷;
  2. AQS底層原理:在lock獲取鎖時首先判斷當前鎖是否可以用(AQS的state狀態值是否爲0),如果是 直接“插隊”獲取鎖,否則進入排隊隊列,並阻塞當前線程; 充分利用了喚醒線程的時間【Singel標誌喚醒,需要前驅節點喚醒】

 

非公平鎖加鎖的簡單步驟

基於CAS嘗試將state(鎖數量)從0設置爲1 ---第一次插隊

  1. 如果設置成功,設置當前線程爲獨佔鎖的線程;
  2. 如果設置失敗,還會再獲取一次鎖數量,---第二次插隊
    1. 如果鎖數量爲0,再基於CAS嘗試將state(鎖數量)從0設置爲1一次,如果設置成功,設置當前線程爲獨佔鎖的線程;
    2. 如果鎖數量不爲0或者上邊的嘗試又失敗了,查看當前線程是不是已經是獨佔鎖的線程了,如果是,則將當前的鎖數量+1;如果不是,則將該線程封裝在一個Node內,並加入到等待隊列中去。等待被其前一個線程節點喚醒
  3. 入隊後,無限循環tryAcquire(1)方法 ---第三次插隊

 

非公平鎖源碼

1.基於CAS將state(鎖數量)從0設置爲1,如果設置成功,設置當前線程爲獨佔鎖的線程;-->第一次插隊

若失敗,調用acquire(1)->tryAcquire(1),acquireQueued(addWaiter(Node.EXCLUSIVE)

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//請求鎖成功,中斷自己 }

tryAcquire(arg)會調用nonfairTryAcquire(1)調用,作用:第二次插隊請求鎖

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//獲取當前線程 int c = getState();//獲取鎖數量 if (c == 0) {//如果鎖數量爲0,證明該獨佔鎖已被釋放,當下沒有線程在使用 if (compareAndSetState(0, acquires)) {//繼續通過CAS將state由0變爲1,注意這裏傳入的acquires爲1 setExclusiveOwnerThread(current);//將當前線程設置爲獨佔鎖的線程 return true; } } else if (current == getExclusiveOwnerThread()) {//查看當前線程是不是就是獨佔鎖的線程 int nextc = c + acquires;//如果是,鎖狀態的數量爲當前的鎖數量+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//設置當前的鎖數量 return true; } return false; }

若請求鎖失敗,將當前線程鏈入隊尾並掛起,之後等待被喚醒 【快速、正常】

  1. 首先會使用addWaiter(Node.EXCLUSIVE)將當前線程封裝進Node節點node,然後將該節點加入等待隊列
  2. 先快速入隊【存在尾節點,將使用CAS嘗試將尾節點設置爲node】
  3. 如果快速入隊不成功【尾節點爲空】,使用正常入隊方法enq,無限循環=第一次阻塞,直到Node節點入隊爲止【創建一個dummy節點,並將該節點通過CAS設置到頭節點,若頭結點不爲null,cas繼續快速入隊】

入隊成功後返回node節點,繼續第三次插隊

無限循環調用:acquireQueued(final Node node, int arg)獲取node的前驅節點p

p==head&&tryAcquire(1) 是唯一跳出循環的方法:p成爲頭結點並且獲取鎖成功:如果p是頭節點,就繼續使用tryAcquire(1)方法插隊,若成功,不用中斷,第三次插隊成功

  1. 如果p不是頭節點,或者tryAcquire(1)請求不成功,執行shouldParkAfterFailedAcquire(Node pred, Node node)來檢測當前節點是不是可以安全的被掛起:判斷p的等待狀態waitStatus
    1. SIGNAL(即可以喚醒下一個節點的線程),則node節點的線程可以安全掛起,返回true
    2. CANCELLED,則p的線程被取消了,我們會將p之前的連續幾個被取消的前驅節點從隊列中剔除
    3. 等待狀態是除了上述兩種的其他狀態,CAS嘗試將前驅節點的等待狀態設爲SIGNAL【p與node競爭】

掛起後後 跳出循環,需要中斷自身

LockSupport.park(this);//掛起當前的線程,後等待前去節點unpark喚醒該線程;方法爲public

 

DelayQueue中的使用示例:

  1. take()和offer()都是lock了重入鎖,按照synchronized的公平鎖,兩個方法是互斥
  2. take()方法需要等待1個小時才能返回,offer()需要馬上提交一個10秒後運行的任務,此時offer()可以插隊獲取鎖
  3. 原理:A執行時,B lock()鎖,並休眠;當鎖被A釋放處於可用狀態時,B線程卻還處於被喚醒的過程中,此時C線程請求鎖,可以優先C得到鎖

 

Reentrantlock優點

  1. 顯示鎖可中斷,防止死鎖,內置鎖不可中斷,會產生死鎖
  2. 實現其他特性的鎖
  3. 對鎖更精細的控制

 

synchronized優點

  1. 顯示鎖易忘記 finally 塊釋放鎖,對程序有害
  2. 顯示鎖只能用在代碼塊,強制更細粒度的加鎖;syn可以用在方法上
  3. synchronized 管理鎖定和釋放時,能標識死鎖或者其他異常行爲的來源,利於調試
  4. Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,兩者的性能就差不多

使用場景

  1. Condition類對鎖進行更精確的控制,指定喚醒、分組喚醒
  2. 防止死鎖
  3. 輪詢鎖:用tryLock(long timeout, TimeUnit unit)和tryLock() 這兩個方法實現,即沒有獲取到鎖,可以使用while循環 隔一段時間再次獲取,直到獲取到爲止
  4. 定時鎖:指的是在指定時間內沒有獲取到鎖,就取消阻塞並返回獲取鎖失敗;tryLock(long timeout, TimeUnit unit)
  5. 可中斷鎖:lockInterruptibly,防止死鎖

 

區別

synchronied是JVM級別的,而ReentrantLock是api級別的

JVM會對synchronied做出相應的優化

鎖消除:JVM判斷堆上的數據不會逃逸出當前線程,不加鎖;

自旋鎖

自適應鎖

 

提供的lock()方法:

重入鎖實現 直到state爲0,其他鎖纔可以用

  1. 如果該鎖沒有被另一個線程持有,則獲取該鎖並立即返回,將鎖計數設置爲 1;對應AQS中的state
  2. 如果當前線程已經持有該鎖,將鎖計數加 1,並立即返回方法---重入鎖
  3. 如果該鎖被另一個線程持有,則禁用當前線程,在獲得鎖之前,一直休眠,此時鎖保持計數設置爲 1

排他鎖實現:Lock類有讀鎖和寫鎖,讀讀共享,寫寫互斥,讀寫互斥:每次獲取鎖時都是首先判斷state是否爲0,並且只有1個線程能獲取到鎖

tryLock和lock和lockInterruptibly的區別

  1. tryLock能獲得鎖就返回true,不能就立即返回false,可以增加時間限制,如果超過該時間段還沒獲得鎖,返回false;tryLock(long timeout,TimeUnit unit),
  2. lock能獲得鎖就返回true,不能的話一直等待獲得鎖
  3. lockInterruptibly,中斷會拋出異常

 

鎖的Condition類

Lock類可以創建Condition對象,Condition對象用來是線程等待和喚醒線程;Condition condition=lock.newCondition();

對鎖進行更精確的控制

  1. Condition中的await()方法相當於Object的wait()方法
  2. Condition中的signal()方法相當於Object的notify()方法
  3. Condition中的signalAll()相當於Object的notifyAll()方法
  4. ReentrantLock類可以喚醒指定條件的線程,而object的喚醒是隨機的

 

Condition函數列表

  1. 造成當前線程在接到信號或被中斷之前一直處於等待狀態 void await()
  2. 喚醒一個等待線程 void signal()
  3. 喚醒所有等待線程 void signalAll()
  4. 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態 boolean await(long time, TimeUnit unit)
  5. 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態 long awaitNanos(long nanosTimeout)
  6. 造成當前線程在接到信號之前一直處於等待狀態 void awaitUninterruptibly()
  7. 造成當前線程在接到信號、被中斷或到達指定最後期限之前一直處於等待狀態 boolean awaitUntil(Date deadline)

 

 

共享鎖實現 併發讀 ReentrantReadWriteLock、計數器

共享鎖的AQS實現

實現tryAcquireShared方法【檢查下一個節點是共享節點】,獲取共享鎖,鎖在所有調用await方法的線程間共享

底層:

  1. 在AQS隊列中,將線程包裝爲Node.SHARED節點,即標誌爲共享鎖
  2. 當頭節點獲得共享鎖後,喚醒下一個共享類型結點的操作
    1. 頭節點node1調用unparkSuccessor()方法喚醒了Node2,並且調用tryAcquireShared方法檢查下一個節點是共享節點
    2. 如果是,更改頭結點,重複以上步驟,以實現節點自身獲取共享鎖成功後,喚醒下一個共享類型結點的操作

應用:

1.ReentrantReadWriteLock

2.CountDownLatch爲java.util.concurrent包下的計數器工具類

可被多線程併發的實現減1操作,並在計數器爲0後,調用await方法的線程被喚醒,從而實現多線程間的協作

new CountDownLatch(3).countDown();

用來實現等所有共享鎖線程都喚醒後一起協作

 

死鎖

多個線程因競爭資源而造成僵局(互相等待),無法向前推進

產生的原因

1) 系統資源的競爭

系統不可剝奪資源,數量不足以滿足多個進程運行,使得進程在運行過程中,因競爭資源而陷入僵局

2) 進程推進順序非法

請求和釋放資源的順序不當,也同樣會導致死鎖。如,互相申請各佔有的資源。

信號量使用不當也會造成死鎖。進程間彼此相互等待消息,結果也會使得這 些進程間無法繼續向前推進。

3) 死鎖產生的4個必要條件,只要其中任一條件不成立,死鎖就不會發生

  1. 資源互斥條件:資源互斥,即某資源僅爲一個進程佔有
  2. 資源不可剝奪條件:進程所獲得的資源在未使用完畢之前,只能是主動釋放,不能被其他進程強行奪走
  3. 保持和請求條件:進程已經保持了一個資源,又提出了新的資源請求,而該資源已被其他進程佔有
  4. 循環等待條件:進程資源循環等待

 

如何避免死鎖

  1. 加鎖順序(線程按照一定的順序加鎖)
    1. 按照順序加鎖是一種死鎖預防機制,需要事先知道所有會用到的鎖
  2. 加鎖時限(超時則放棄)
    1. 獲取鎖時加上時限,超過時限則放棄請求,並釋放鎖,等待一段隨機的時間再重試
  3. 死鎖檢測與恢復
    1. 操作系統中:系統爲進程分配資源,不採取任何限制性措施,提供檢測和恢復的手段

死鎖檢測:當一個線程請求鎖失敗時,遍歷鎖的關係圖檢測死鎖

死鎖恢復

  1. 撤消進程,剝奪資源
  2. 線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖一樣繼續保持着它們需要的鎖
  3. 死鎖發生的時候設置隨機的優先級;如果賦予這些線程的優先級是固定不變的,同一批線程總是會擁有更高的優先級。

 

鎖模式包括: 

  1. 共享鎖:(讀取)用戶可以併發讀取數據,但不能獲取寫鎖,直到釋放所有讀鎖。
  2. 排他鎖(寫鎖):加上寫鎖後,其他線程無法加任何鎖;寫鎖可以讀和寫
  3. 更新鎖: 防止死鎖而設立,轉換讀鎖爲寫鎖之前的準備,僅一個線程可獲得更新鎖

樂觀鎖:認爲數據一般情況下不會造成衝突,在數據提交更新時,才進行數據的衝突檢測;

如果衝突,返回信息讓用戶決定如何去做。實現方式:記錄數據版本。

悲觀鎖:操作數據時上鎖保護,限制其他線程訪問,直到該鎖釋放。關係型數據庫鎖機制,行鎖、頁鎖、表鎖,都是在做操作之前先上鎖。

鎖的粒度: 都是悲觀鎖

  1. 行鎖: 粒度最小,併發性最高
  2. 頁鎖:鎖定一頁。25個行鎖可升級爲一個頁鎖。
  3. 表鎖:粒度大,併發性低
  4. 數據庫鎖:控制整個數據庫操作

 

Happen-Before原則 八大原則:

  1. 定義了線程、鎖、volatile變量、對象創建的先後關係
  2. 若滿足,則保證一個操作執行的結果需要對另一個操作可見
  3. 判斷數據是否存在競爭、線程是否安全的依據
  • 單線程:在同一個線程中,書寫在前面的操作happen-before後面的操作。
  • 鎖:解鎖先於鎖定;同一個鎖的unlock操作happen-before此鎖的lock操作。
  • volatile:先寫;對一個volatile變量的寫操作happen-before對此變量的任意操作(當然也包括寫操作了)。
  • 傳遞性原則:如果A操作 happen-before B操作,B操作happen-before C操作,那麼A操作happen-before C操作。
  • 線程啓動:start方法優先;同一個線程的start方法happen-before此線程的其它方法。
  • 線程中斷:對線程interrupt方法的調用happen-before被中斷線程的檢測到中斷髮送的代碼。
  • 線程終結:線程中的所有操作都happen-before線程的終止檢測。
  • 對象創建:先初始化,後finalize;一個對象的初始化完成先於他的finalize方法調用。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章