深刻理解Java併發框架AQS系列(四):共享鎖(Shared Lock)

深刻理解Java併發框架AQS系列(一):線程
深刻理解Java併發框架AQS系列(二):AQS框架簡介及鎖概念
深刻理解Java併發框架AQS系列(三):獨佔鎖(Exclusive Lock)
深刻理解Java併發框架AQS系列(四):共享鎖(Shared Lock)html

1、前言

那些「簡單的」併發代碼背後,隱藏着大量信息。。。數據結構

獨佔鎖雖然說在j.u.c中有現成的實現,但在JAVA的語言層面也一樣提供了支持(synchronized);但共享鎖倒是隻存在於AQS中,而它在實際生產中的使用頻次絲絕不亞於獨佔鎖,在整個AQS體系中佔有舉重若輕的地位。而在某種意義上,由於可能同時存在多個線程的併發,它的複雜度要高於獨佔鎖。本章除了介紹共享鎖數據結構等,還會重點對焦併發處理,看 doug lea 在併發部分是否有遺漏併發

j.u.c下支持的併發鎖有SemaphoreCountDownLatch等,本章咱們採用經典併發類Semaphore來闡述框架

2、簡介

共享鎖實際上是相對獨佔鎖而言的,涉及到共享鎖就要聊到併發度,即同一時刻最多容許同時執行線程的數量。上圖所述的併發度爲3,即在同一時刻,最多可有3我的在同時過河。高併發

但共享鎖的併發度也能夠設置爲1,此時它能夠看做是一個特殊的獨佔鎖oop

2.一、waitStatus

在獨佔鎖章節中,咱們介紹到了關鍵的狀態標記字段waitStatus,它在獨佔鎖的取值有性能

  • 0
  • SIGNAL (-1)
  • CANCELLED (1)

而這些取值在共享鎖中也都存在,含義也保持一致,而除了上述這3個取值外,共享鎖還額外引入了新的取值:測試

  • PROPAGATE (-3)

-3這個取值在整個AQS體系中,只存在於共享鎖中,它的存在是爲了更好的解決併發問題,咱們將在後文中詳細介紹ui

2.二、使用場景

本人蔘加的某性能挑戰賽中,有這樣一個場景:數據產生於CPU,且有12個線程在不斷的製造數據,而這些數據須要持久化到磁盤中,因爲數據產生的很是快,此時的瓶頸卡在IO上;磁盤的性能通過基準測試,發現每次寫入8K數據,且開4個線程寫入時,能將IO打滿;但如何控制在同一時刻,最多有4個線程進行IO寫入呢?線程

其實這是一個典型的使用共享鎖的場景,咱們用三四行代碼便可解決

// 設置共享鎖的併發度爲4
Semaphore semaphore = new Semaphore(4);
// 加鎖
semaphore.acquire();
// 執行數據存儲
storeIO();
// 釋放鎖
semaphore.release();

3、併發

3.一、獨佔鎖 vs 共享鎖

共享鎖的總體流程與獨佔鎖類似,都是首先嚐試去獲取資源(子類邏輯,通常是CAS操做

  • 若是能拿到資源,那麼進入同步塊執行業務代碼;當同步塊執行完畢後,喚醒阻塞隊列的頭結點
  • 若是資源已空,那麼進入阻塞隊列並掛起,等待被其餘線程喚醒

二者的不一樣點在什麼地方呢?就在於「喚醒阻塞隊列的頭結點」的操做。在獨佔鎖時,喚醒頭結點的操做,只會有一個線程(加鎖成功的線程調用release())去觸發;而在共享鎖時,可能會有多個線程同時去調用釋放

直觀感受這樣設計不太合理:若是多個線程同時去喚醒頭結點,而頭結點只能被喚醒一次,假定阻塞隊列中有20個節點,那這些節點只能等待上一個節點執行完畢後纔會被喚醒,無形中共享鎖的併發度變成了1。要解決這個疑問,咱們先來看共享鎖的釋放邏輯

3.二、鎖釋放

先來思考一下鎖釋放須要作的事兒

  • 一、阻塞隊列的第一個節點必定要被激活;這個問題看似不值一提,卻至關重要,區別於獨佔鎖,共享鎖的鎖釋放是存在併發的,在高併發的流量下,必定要保證阻塞隊列的第一個有效節點被激活,不然會致使阻塞隊列永久性的掛死
  • 二、保證激活阻塞隊列時的併發度;這個問題一樣也是獨佔鎖不存在的,也就是咱們在3.1提出的問題;假定這樣一種場景:「共享鎖的併發度爲10,阻塞隊列中有100個待處理的節點,而此時又沒有新的加鎖請求,如何保證在激活阻塞隊列時,保持10的併發度?」

共享鎖如何解決這兩個問題呢?咱們接下來逐一闡述

3.2.一、調用點

與獨佔鎖不一樣,共享鎖調用「鎖釋放」有2個地方(注:AQS的一個阻塞隊列是能夠同時添加獨佔節點、共享節點的,爲了簡化模型,咱們這裏暫不討論這種混合模型

  • a、某線程同步塊執行完畢,正常調用解鎖邏輯;此點與獨佔鎖一致
  • b、在每次更換頭結點時,若是知足如下任一條件,一樣會調用「鎖釋放」;更換頭結點的操做,其實此時已經意味着當前線程已經加鎖成功
    • b.一、有額外的資源可用;拿信號量舉例,當發現信號量數量>0時,表示有額外資源可用
    • b.二、舊的頭結點或當前頭結點的ws < 0

那這兩個點調用的時候,是否存在併發呢?有同窗會說「a存在併發,b是串行的」;其實此處b也是存在併發的,例如線程1更換了head節點後,準備執行「鎖釋放」邏輯,正在此時,線程2正常鎖釋放後,喚醒了新的head節點(線程3),線程3又會執行更換head節點,並準備執行「鎖釋放」邏輯;此時線程1跟線程3都準備執行「鎖釋放」邏輯

共享鎖解鎖邏輯的調用點

既然「鎖釋放」存在這麼多併發,那就必定要保證「鎖釋放」邏輯是冪等的,那它又是如何作到呢?

3.2.一、鎖釋放

直接貼一下它的源碼吧,釋放鎖的代碼寥寥幾筆,卻很難說它簡單

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

對應的流程圖以下:

共享鎖釋放鎖流程

咱們簡單描述一下鎖釋放作的事兒

  • 一、首選獲取頭結點的快照,並將其賦予變量h,同時獲取h.waitStatus,並標記位ws
  • 二、判斷ws的狀態
    • ws == -1 表示下一個節點已經掛起,或即將掛起。若是隻要發現是-1狀態,就進行線程喚起的話,由於存在併發,可能致使目標線程被喚起屢次,故此處須要經過CAS進行搶鎖,保證只有一個線程去喚起
    • ws == 0 若是發現節點ws爲0,此處會存在兩種狀況(狀況1:節點剛新建完畢,還未進入阻塞隊列;狀況2:節點由-1修改成了0),無論哪一種狀況,都強制將其由-1改成-3,標記位強制傳播,此處是否存在漏洞?
    • ws == -3 表示當前節點已經被標識爲強制傳播了,直接結束
  • 三、若是此時 h == head,說明在上述邏輯發生時,頭結點沒有發生變化,那麼結束當前操做,不然重複上述步驟。注:AQS中全部節點只有一次當頭結點的機會,也就是某個節點當過一次頭結點後,便會被拋棄,再無可能第二次成爲頭結點,這點相當重要

根據以上分析,咱們發現,節點的狀態流轉是經過ws來控制的,即0、-一、-3,乍看上去,貌似不太嚴謹,那咱們來作具體分析

3.2.二、ws狀態流轉

僅有2個功能點會對ws進行修改,一是將節點加入阻塞隊列時,二就是3.2.1中描述的調用鎖釋放邏輯時;

咱們將加入阻塞隊列時ws的狀態流轉再回憶下:

  • 狀態爲0(初始狀態),加入阻塞隊列前,須要將前節點修改成-1,而後進入線程掛起
  • 狀態爲-3(強制傳播狀態,被解鎖線程標記),加入阻塞隊列前,一樣須要將前節點修改成-1,而後進入線程掛起

綜述,咱們出一張ws的總體狀態流轉圖

共享鎖加入阻塞隊列時ws流轉

由上圖可得知,只要解鎖邏輯成功經過CAS將head節點由-1修改成0的話,那麼就要負責喚醒阻塞隊列中的第一個節點了

整個流轉過程有bug嗎?咱們設想以下場景:共享鎖的併發度設置爲1,A、B兩個線程同時進入加鎖邏輯,B線程成功搶到鎖,並開始進入同步塊,A線程搶鎖失敗,準備掛到阻塞隊列,正常流程是A線程將ws由0修改成-1後,進入掛起狀態,但B線程執行較快,已經優先A線程並開始執行解鎖邏輯,將ws由0修改成了-3,而後B線程正常結束;A線程發現ws爲-3後,將其修改成-1,而後進入掛起。 若是這個場景真實發生的話,A線程將永久處於掛起狀態,那豈不是存在漏洞?

然而事實並不是如此,由於只要A線程將ws修改成-1後,都要再嘗試進行一次獲取鎖的操做,正是這個操做避免了上述狀況的發生,可見aqs是很嚴謹的

共享鎖加入阻塞隊列及解鎖ws流轉示意圖

3.三、保證併發度

阻塞隊列中節點的激活順序是什麼樣呢?其實激活順序3.2章節已經描述的較爲清楚,解鎖的邏輯只負責激活頭節點,那如何保證共享鎖的併發度?

咱們仍是假定這樣一個場景:共享鎖的併發度爲5,阻塞隊列中有20個節點,只有head節點已被喚醒,且沒有新的請求進入,咱們但願在同一時刻,同時有5個節點處於激活狀態。針對上述場景,aqs如何作到呢?

共享鎖阻塞隊列併發度

其實head節點被激活時,在第一時間會通知後續節點,並將其喚醒,而後纔會執行同步塊邏輯,保證了等待中的節點快速激活

相關文章
相關標籤/搜索