<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          萬字長(zhǎng)文,一文讀懂Java 中的各種鎖

          共 22695字,需瀏覽 46分鐘

           ·

          2020-01-04 23:25

          來源:Java建設(shè)者作者:cxuan


          Java 鎖分類

          Java 中的鎖有很多,可以按照不同的功能、種類進(jìn)行分類,下面是我對(duì) Java 中一些常用鎖的分類,包括一些基本的概述

          6c22ae0879b85f4bc661ff5510979946.webp

          • 從線程是否需要對(duì)資源加鎖可以分為 悲觀鎖樂觀鎖
          • 從資源已被鎖定,線程是否阻塞可以分為 自旋鎖
          • 從多個(gè)線程并發(fā)訪問資源,也就是 Synchronized 可以分為 無鎖偏向鎖輕量級(jí)鎖重量級(jí)鎖
          • 從鎖的公平性進(jìn)行區(qū)分,可以分為公平鎖非公平鎖
          • 從根據(jù)鎖是否重復(fù)獲取可以分為 可重入鎖不可重入鎖
          • 從那個(gè)多個(gè)線程能否獲取同一把鎖分為 共享鎖排他鎖

          下面我們依次對(duì)各個(gè)鎖的分類進(jìn)行詳細(xì)闡述。

          線程是否需要對(duì)資源加鎖

          Java 按照是否對(duì)資源加鎖分為樂觀鎖悲觀鎖,樂觀鎖和悲觀鎖并不是一種真實(shí)存在的鎖,而是一種設(shè)計(jì)思想,樂觀鎖和悲觀鎖對(duì)于理解 Java 多線程和數(shù)據(jù)庫(kù)來說至關(guān)重要,下面就來探討一下這兩種實(shí)現(xiàn)方式的區(qū)別和優(yōu)缺點(diǎn)

          悲觀鎖

          悲觀鎖是一種悲觀思想,它總認(rèn)為最壞的情況可能會(huì)出現(xiàn),它認(rèn)為數(shù)據(jù)很可能會(huì)被其他人所修改,所以悲觀鎖在持有數(shù)據(jù)的時(shí)候總會(huì)把資源 或者 數(shù)據(jù) 鎖住,這樣其他線程想要請(qǐng)求這個(gè)資源的時(shí)候就會(huì)阻塞,直到等到悲觀鎖把資源釋放為止。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,**比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。**悲觀鎖的實(shí)現(xiàn)往往依靠數(shù)據(jù)庫(kù)本身的鎖功能實(shí)現(xiàn)。

          Java 中的 SynchronizedReentrantLock 等獨(dú)占鎖(排他鎖)也是一種悲觀鎖思想的實(shí)現(xiàn),因?yàn)?Synchronzied 和 ReetrantLock 不管是否持有資源,它都會(huì)嘗試去加鎖,生怕自己心愛的寶貝被別人拿走。

          樂觀鎖

          樂觀鎖的思想與悲觀鎖的思想相反,它總認(rèn)為資源和數(shù)據(jù)不會(huì)被別人所修改,所以讀取不會(huì)上鎖,但是樂觀鎖在進(jìn)行寫入操作的時(shí)候會(huì)判斷當(dāng)前數(shù)據(jù)是否被修改過(具體如何判斷我們下面再說)。樂觀鎖的實(shí)現(xiàn)方案一般來說有兩種:版本號(hào)機(jī)制CAS實(shí)現(xiàn) 。樂觀鎖多適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。

          在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實(shí)現(xiàn)方式 CAS 實(shí)現(xiàn)的。

          兩種鎖的使用場(chǎng)景

          上面介紹了兩種鎖的基本概念,并提到了兩種鎖的適用場(chǎng)景,一般來說,悲觀鎖不僅會(huì)對(duì)寫操作加鎖還會(huì)對(duì)讀操作加鎖,一個(gè)典型的悲觀鎖調(diào)用:

          select * from student where name="cxuan" for update

          這條 sql 語句從 Student 表中選取 name = "cxuan" 的記錄并對(duì)其加鎖,那么其他寫操作再這個(gè)事務(wù)提交之前都不會(huì)對(duì)這條數(shù)據(jù)進(jìn)行操作,起到了獨(dú)占和排他的作用。

          悲觀鎖因?yàn)閷?duì)讀寫都加鎖,所以它的性能比較低,對(duì)于現(xiàn)在互聯(lián)網(wǎng)提倡的三高(高性能、高可用、高并發(fā))來說,悲觀鎖的實(shí)現(xiàn)用的越來越少了,但是一般多讀的情況下還是需要使用悲觀鎖的,因?yàn)殡m然加鎖的性能比較低,但是也阻止了像樂觀鎖一樣,遇到寫不一致的情況下一直重試的時(shí)間。

          相對(duì)而言,樂觀鎖用于讀多寫少的情況,即很少發(fā)生沖突的場(chǎng)景,這樣可以省去鎖的開銷,增加系統(tǒng)的吞吐量。

          樂觀鎖的適用場(chǎng)景有很多,典型的比如說成本系統(tǒng),柜員要對(duì)一筆金額做修改,為了保證數(shù)據(jù)的準(zhǔn)確性和實(shí)效性,使用悲觀鎖鎖住某個(gè)數(shù)據(jù)后,再遇到其他需要修改數(shù)據(jù)的操作,那么此操作就無法完成金額的修改,對(duì)產(chǎn)品來說是災(zāi)難性的一刻,使用樂觀鎖的版本號(hào)機(jī)制能夠解決這個(gè)問題,我們下面說。

          樂觀鎖的實(shí)現(xiàn)方式

          樂觀鎖一般有兩種實(shí)現(xiàn)方式:采用版本號(hào)機(jī)制CAS(Compare-and-Swap,即比較并替換)算法實(shí)現(xiàn)。

          版本號(hào)機(jī)制

          版本號(hào)機(jī)制是在數(shù)據(jù)表中加上一個(gè) version 字段來實(shí)現(xiàn)的,表示數(shù)據(jù)被修改的次數(shù),當(dāng)執(zhí)行寫操作并且寫入成功后,version = version + 1,當(dāng)線程A要更新數(shù)據(jù)時(shí),在讀取數(shù)據(jù)的同時(shí)也會(huì)讀取 version 值,在提交更新時(shí),若剛才讀取到的 version 值為當(dāng)前數(shù)據(jù)庫(kù)中的version值相等時(shí)才更新,否則重試更新操作,直到更新成功。

          我們以上面的金融系統(tǒng)為例,來簡(jiǎn)述一下這個(gè)過程。

          311b0832343b13c7ed355315522a9ba1.webp

          • 成本系統(tǒng)中有一個(gè)數(shù)據(jù)表,表中有兩個(gè)字段分別是 金額version,金額的屬性是能夠?qū)崟r(shí)變化,而 version 表示的是金額每次發(fā)生變化的版本,一般的策略是,當(dāng)金額發(fā)生改變時(shí),version 采用遞增的策略每次都在上一個(gè)版本號(hào)的基礎(chǔ)上 + 1。
          • 在了解了基本情況和基本信息之后,我們來看一下這個(gè)過程:公司收到回款后,需要把這筆錢放在金庫(kù)中,假如金庫(kù)中存有100 元錢
            • 下面開啟事務(wù)一:當(dāng)男柜員執(zhí)行回款寫入操作前,他會(huì)先查看(讀)一下金庫(kù)中還有多少錢,此時(shí)讀到金庫(kù)中有 100 元,可以執(zhí)行寫操作,并把數(shù)據(jù)庫(kù)中的錢更新為 120 元,提交事務(wù),金庫(kù)中的錢由 100 -> 120,version的版本號(hào)由 0 -> 1。
            • 開啟事務(wù)二:女柜員收到給員工發(fā)工資的請(qǐng)求后,需要先執(zhí)行讀請(qǐng)求,查看金庫(kù)中的錢還有多少,此時(shí)的版本號(hào)是多少,然后從金庫(kù)中取出員工的工資進(jìn)行發(fā)放,提交事務(wù),成功后版本 + 1,此時(shí)版本由 1 -> 2。

          上面兩種情況是最樂觀的情況,上面的兩個(gè)事務(wù)都是順序執(zhí)行的,也就是事務(wù)一和事務(wù)二互不干擾,那么事務(wù)要并行執(zhí)行會(huì)如何呢?

          fc46a2f4f6b8e04e81bea7db2f4b68b1.webp

          • 事務(wù)一開啟,男柜員先執(zhí)行讀操作,取出金額和版本號(hào),執(zhí)行寫操作

            begin
            update 表 set 金額 = 120,version = version + 1 where 金額 = 100 and version = 0

            此時(shí)金額改為 120,版本號(hào)為1,事務(wù)還沒有提交

            事務(wù)二開啟,女柜員先執(zhí)行讀操作,取出金額和版本號(hào),執(zhí)行寫操作

            begin
            update 表 set 金額 = 50,version = version + 1 where 金額 = 100 and version = 0

            此時(shí)金額改為 50,版本號(hào)變?yōu)?1,事務(wù)未提交

            現(xiàn)在提交事務(wù)一,金額改為 120,版本變?yōu)?,提交事務(wù)。理想情況下應(yīng)該變?yōu)?金額 = 50,版本號(hào) = 2,但是實(shí)際上事務(wù)二 的更新是建立在金額為 100 和 版本號(hào)為 0 的基礎(chǔ)上的,所以事務(wù)二不會(huì)提交成功,應(yīng)該重新讀取金額和版本號(hào),再次進(jìn)行寫操作。

            這樣,就避免了女柜員 用基于 version = 0 的舊數(shù)據(jù)修改的結(jié)果覆蓋男操作員操作結(jié)果的可能。

          CAS 算法

          省略代碼,完整代碼請(qǐng)參照 看完你就應(yīng)該能明白的悲觀鎖和樂觀鎖

          CAS 即 compare and swap(比較與交換),是一種有名的無鎖算法。即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization

          Java 從 JDK1.5 開始支持,java.util.concurrent 包里提供了很多面向并發(fā)編程的類,也提供了 CAS 算法的支持,一些以 Atomic 為開頭的一些原子類都使用 CAS 作為其實(shí)現(xiàn)方式。使用這些類在多核 CPU 的機(jī)器上會(huì)有比較好的性能。

          如果要保證它們的原子性,必須進(jìn)行加鎖,使用 Synchronzied 或者 ReentrantLock,我們前面介紹它們是悲觀鎖的實(shí)現(xiàn),我們現(xiàn)在討論的是樂觀鎖,那么用哪種方式保證它們的原子性呢?請(qǐng)繼續(xù)往下看

          CAS 中涉及三個(gè)要素:

          • 需要讀寫的內(nèi)存值 V
          • 進(jìn)行比較的值 A
          • 擬寫入的新值 B

          當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。

          我們以 java.util.concurrent 中的AtomicInteger 為例,看一下在不用鎖的情況下是如何保證線程安全的

          public class AtomicCounter {

          private AtomicInteger integer = new AtomicInteger();

          public AtomicInteger getInteger() {
          return integer;
          }

          public void setInteger(AtomicInteger integer) {
          this.integer = integer;
          }

          public void increment(){
          integer.incrementAndGet();
          }

          public void decrement(){
          integer.decrementAndGet();
          }

          }

          public class AtomicProducer extends Thread{

          private AtomicCounter atomicCounter;

          public AtomicProducer(AtomicCounter atomicCounter){
          this.atomicCounter = atomicCounter;
          }

          @Override
          public void run() {
          for(int j = 0; j < AtomicTest.LOOP; j++) {
          System.out.println("producer : " + atomicCounter.getInteger());
          atomicCounter.increment();
          }
          }
          }

          public class AtomicConsumer extends Thread{

          private AtomicCounter atomicCounter;

          public AtomicConsumer(AtomicCounter atomicCounter){
          this.atomicCounter = atomicCounter;
          }

          @Override
          public void run() {
          for(int j = 0; j < AtomicTest.LOOP; j++) {
          System.out.println("consumer : " + atomicCounter.getInteger());
          atomicCounter.decrement();
          }
          }
          }

          public class AtomicTest {

          final static int LOOP = 10000;

          public static void main(String[] args) throws InterruptedException {

          AtomicCounter counter = new AtomicCounter();
          AtomicProducer producer = new AtomicProducer(counter);
          AtomicConsumer consumer = new AtomicConsumer(counter);

          producer.start();
          consumer.start();

          producer.join();
          consumer.join();

          System.out.println(counter.getInteger());

          }
          }

          經(jīng)測(cè)試可得,不管循環(huán)多少次最后的結(jié)果都是0,也就是多線程并行的情況下,使用 AtomicInteger 可以保證線程安全性。incrementAndGet 和 decrementAndGet 都是原子性操作。

          樂觀鎖的缺點(diǎn)

          任何事情都是有利也有弊,軟件行業(yè)沒有完美的解決方案只有最優(yōu)的解決方案,所以樂觀鎖也有它的弱點(diǎn)和缺陷:

          ABA 問題

          ABA 問題說的是,如果一個(gè)變量第一次讀取的值是 A,準(zhǔn)備好需要對(duì) A 進(jìn)行寫操作的時(shí)候,發(fā)現(xiàn)值還是 A,那么這種情況下,能認(rèn)為 A 的值沒有被改變過嗎?可以是由 A -> B -> A 的這種情況,但是 AtomicInteger 卻不會(huì)這么認(rèn)為,它只相信它看到的,它看到的是什么就是什么。

          JDK 1.5 以后的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。

          也可以采用CAS的一個(gè)變種DCAS來解決這個(gè)問題。DCAS,是對(duì)于每一個(gè)V增加一個(gè)引用的表示修改次數(shù)的標(biāo)記符。對(duì)于每個(gè)V,如果引用修改了一次,這個(gè)計(jì)數(shù)器就加1。然后在這個(gè)變量需要update的時(shí)候,就同時(shí)檢查變量的值和計(jì)數(shù)器的值。

          循環(huán)開銷大

          我們知道樂觀鎖在進(jìn)行寫操作的時(shí)候會(huì)判斷是否能夠?qū)懭氤晒Γ绻麑懭氩怀晒⒂|發(fā)等待 -> 重試機(jī)制,這種情況是一個(gè)自旋鎖,簡(jiǎn)單來說就是適用于短期內(nèi)獲取不到,進(jìn)行等待重試的鎖,它不適用于長(zhǎng)期獲取不到鎖的情況,另外,自旋循環(huán)對(duì)于性能開銷比較大。

          CAS與synchronized的使用情景

          簡(jiǎn)單的來說 CAS 適用于寫比較少的情況下(多讀場(chǎng)景,沖突一般較少),synchronized 適用于寫比較多的情況下(多寫場(chǎng)景,沖突一般較多)

          • 對(duì)于資源競(jìng)爭(zhēng)較少(線程沖突較輕)的情況,使用 Synchronized 同步鎖進(jìn)行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費(fèi)消耗 cpu 資源;而 CAS 基于硬件實(shí)現(xiàn),不需要進(jìn)入內(nèi)核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
          • 對(duì)于資源競(jìng)爭(zhēng)嚴(yán)重(線程沖突嚴(yán)重)的情況,CAS 自旋的概率會(huì)比較大,從而浪費(fèi)更多的 CPU 資源,效率低于 synchronized。

          資源已被鎖定,線程是否阻塞

          自旋鎖的提出背景

          由于在多處理器環(huán)境中某些資源的有限性,有時(shí)需要互斥訪問(mutual exclusion),這時(shí)候就需要引入鎖的概念,只有獲取了鎖的線程才能夠?qū)Y源進(jìn)行訪問,由于多線程的核心是CPU的時(shí)間分片,所以同一時(shí)刻只能有一個(gè)線程獲取到鎖。那么就面臨一個(gè)問題,那么沒有獲取到鎖的線程應(yīng)該怎么辦?

          通常有兩種處理方式:一種是沒有獲取到鎖的線程就一直循環(huán)等待判斷該資源是否已經(jīng)釋放鎖,這種鎖叫做自旋鎖,它不用將線程阻塞起來(NON-BLOCKING);還有一種處理方式就是把自己阻塞起來,等待重新調(diào)度請(qǐng)求,這種叫做互斥鎖

          什么是自旋鎖

          自旋鎖的定義:當(dāng)一個(gè)線程嘗試去獲取某一把鎖的時(shí)候,如果這個(gè)鎖此時(shí)已經(jīng)被別人獲取(占用),那么此線程就無法獲取到這把鎖,該線程將會(huì)等待,間隔一段時(shí)間后會(huì)再次嘗試獲取。這種采用循環(huán)加鎖 -> 等待的機(jī)制被稱為自旋鎖(spinlock)


          4a975bed4829d921f4b51b52b5e824d3.webp

          自旋鎖的原理

          自旋鎖的原理比較簡(jiǎn)單,如果持有鎖的線程能在短時(shí)間內(nèi)釋放鎖資源,那么那些等待競(jìng)爭(zhēng)鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞狀態(tài),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之后即可獲取,這樣就避免了用戶進(jìn)程和內(nèi)核切換的消耗。

          因?yàn)樽孕i避免了操作系統(tǒng)進(jìn)程調(diào)度和線程切換,所以自旋鎖通常適用在時(shí)間比較短的情況下。由于這個(gè)原因,操作系統(tǒng)的內(nèi)核經(jīng)常使用自旋鎖。但是,如果長(zhǎng)時(shí)間上鎖的話,自旋鎖會(huì)非常耗費(fèi)性能,它阻止了其他線程的運(yùn)行和調(diào)度。線程持有鎖的時(shí)間越長(zhǎng),則持有該鎖的線程將被 OS(Operating System) 調(diào)度程序中斷的風(fēng)險(xiǎn)越大。如果發(fā)生中斷情況,那么其他線程將保持旋轉(zhuǎn)狀態(tài)(反復(fù)嘗試獲取鎖),而持有該鎖的線程并不打算釋放鎖,這樣導(dǎo)致的是結(jié)果是無限期推遲,直到持有鎖的線程可以完成并釋放它為止。

          解決上面這種情況一個(gè)很好的方式是給自旋鎖設(shè)定一個(gè)自旋時(shí)間,等時(shí)間一到立即釋放自旋鎖。自旋鎖的目的是占著CPU資源不進(jìn)行釋放,等到獲取鎖立即進(jìn)行處理。但是如何去選擇自旋時(shí)間呢?如果自旋執(zhí)行時(shí)間太長(zhǎng),會(huì)有大量的線程處于自旋狀態(tài)占用 CPU 資源,進(jìn)而會(huì)影響整體系統(tǒng)的性能。因此自旋的周期選的額外重要!JDK在1.6 引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋時(shí)間不是固定的了,而是由前一次在同一個(gè)鎖上的自旋時(shí)間以及鎖擁有的狀態(tài)來決定,基本認(rèn)為一個(gè)線程上下文切換的時(shí)間是最佳的一個(gè)時(shí)間。

          自旋鎖的優(yōu)缺點(diǎn)

          自旋鎖盡可能的減少線程的阻塞,這對(duì)于鎖的競(jìng)爭(zhēng)不激烈,且占用鎖時(shí)間非常短的代碼塊來說性能能大幅度的提升,因?yàn)樽孕南臅?huì)小于線程阻塞掛起再喚醒的操作的消耗,這些操作會(huì)導(dǎo)致線程發(fā)生兩次上下文切換!

          但是如果鎖的競(jìng)爭(zhēng)激烈,或者持有鎖的線程需要長(zhǎng)時(shí)間占用鎖執(zhí)行同步塊,這時(shí)候就不適合使用自旋鎖了,因?yàn)樽孕i在獲取鎖前一直都是占用 cpu 做無用功,占著 XX 不 XX,同時(shí)有大量線程在競(jìng)爭(zhēng)一個(gè)鎖,會(huì)導(dǎo)致獲取鎖的時(shí)間很長(zhǎng),線程自旋的消耗大于線程阻塞掛起操作的消耗,其它需要 cpu 的線程又不能獲取到 cpu,造成 cpu 的浪費(fèi)。所以這種情況下我們要關(guān)閉自旋鎖。

          自旋鎖的實(shí)現(xiàn)

          下面我們用Java 代碼來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的自旋鎖

          public class SpinLockTest {

          private AtomicBoolean available = new AtomicBoolean(false);

          public void lock(){

          // 循環(huán)檢測(cè)嘗試獲取鎖
          while (!tryLock()){
          // doSomething...
          }

          }

          public boolean tryLock(){
          // 嘗試獲取鎖,成功返回true,失敗返回false
          return available.compareAndSet(false,true);
          }

          public void unLock(){
          if(!available.compareAndSet(true,false)){
          throw new RuntimeException("釋放鎖失敗");
          }
          }

          }

          這種簡(jiǎn)單的自旋鎖有一個(gè)問題:無法保證多線程競(jìng)爭(zhēng)的公平性。對(duì)于上面的 SpinlockTest,當(dāng)多個(gè)線程想要獲取鎖時(shí),誰最先將available設(shè)為false誰就能最先獲得鎖,這可能會(huì)造成某些線程一直都未獲取到鎖造成線程饑餓。就像我們下課后蜂擁的跑向食堂,下班后蜂擁地?cái)D向地鐵,通常我們會(huì)采取排隊(duì)的方式解決這樣的問題,類似地,我們把這種鎖叫排隊(duì)自旋鎖(QueuedSpinlock)。計(jì)算機(jī)科學(xué)家們使用了各種方式來實(shí)現(xiàn)排隊(duì)自旋鎖,如TicketLock,MCSLock,CLHLock。接下來我們分別對(duì)這幾種鎖做個(gè)大致的介紹。

          TicketLock

          在計(jì)算機(jī)科學(xué)領(lǐng)域中,TicketLock 是一種同步機(jī)制或鎖定算法,它是一種自旋鎖,它使用ticket 來控制線程執(zhí)行順序。

          就像票據(jù)隊(duì)列管理系統(tǒng)一樣。面包店或者服務(wù)機(jī)構(gòu)(例如銀行)都會(huì)使用這種方式來為每個(gè)先到達(dá)的顧客記錄其到達(dá)的順序,而不用每次都進(jìn)行排隊(duì)。通常,這種地點(diǎn)都會(huì)有一個(gè)分配器(叫號(hào)器,掛號(hào)器等等都行),先到的人需要在這個(gè)機(jī)器上取出自己現(xiàn)在排隊(duì)的號(hào)碼,這個(gè)號(hào)碼是按照自增的順序進(jìn)行的,旁邊還會(huì)有一個(gè)標(biāo)牌顯示的是正在服務(wù)的標(biāo)志,這通常是代表目前正在服務(wù)的隊(duì)列號(hào),當(dāng)前的號(hào)碼完成服務(wù)后,標(biāo)志牌會(huì)顯示下一個(gè)號(hào)碼可以去服務(wù)了。

          像上面系統(tǒng)一樣,TicketLock 是基于先進(jìn)先出(FIFO) 隊(duì)列的機(jī)制。它增加了鎖的公平性,其設(shè)計(jì)原則如下:TicketLock 中有兩個(gè) int 類型的數(shù)值,開始都是0,第一個(gè)值是隊(duì)列ticket(隊(duì)列票據(jù)), 第二個(gè)值是 出隊(duì)(票據(jù))。隊(duì)列票據(jù)是線程在隊(duì)列中的位置,而出隊(duì)票據(jù)是現(xiàn)在持有鎖的票證的隊(duì)列位置。可能有點(diǎn)模糊不清,簡(jiǎn)單來說,就是隊(duì)列票據(jù)是你取票號(hào)的位置,出隊(duì)票據(jù)是你距離叫號(hào)的位置。現(xiàn)在應(yīng)該明白一些了吧。

          當(dāng)叫號(hào)叫到你的時(shí)候,不能有相同的號(hào)碼同時(shí)辦業(yè)務(wù),必須只有一個(gè)人可以去辦,辦完后,叫號(hào)機(jī)叫到下一個(gè)人,這就叫做原子性。你在辦業(yè)務(wù)的時(shí)候不能被其他人所干擾,而且不可能會(huì)有兩個(gè)持有相同號(hào)碼的人去同時(shí)辦業(yè)務(wù)。然后,下一個(gè)人看自己的號(hào)是否和叫到的號(hào)碼保持一致,如果一致的話,那么就輪到你去辦業(yè)務(wù),否則只能繼續(xù)等待。上面這個(gè)流程的關(guān)鍵點(diǎn)在于,每個(gè)辦業(yè)務(wù)的人在辦完業(yè)務(wù)之后,他必須丟棄自己的號(hào)碼,叫號(hào)機(jī)才能繼續(xù)叫到下面的人,如果這個(gè)人沒有丟棄這個(gè)號(hào)碼,那么其他人只能繼續(xù)等待。下面來實(shí)現(xiàn)一下這個(gè)票據(jù)排隊(duì)方案

          public class TicketLock {

          // 隊(duì)列票據(jù)(當(dāng)前排隊(duì)號(hào)碼)
          private AtomicInteger queueNum = new AtomicInteger();

          // 出隊(duì)票據(jù)(當(dāng)前需等待號(hào)碼)
          private AtomicInteger dueueNum = new AtomicInteger();

          // 獲取鎖:如果獲取成功,返回當(dāng)前線程的排隊(duì)號(hào)
          public int lock(){
          int currentTicketNum = dueueNum.incrementAndGet();
          while (currentTicketNum != queueNum.get()){
          // doSomething...
          }
          return currentTicketNum;
          }

          // 釋放鎖:傳入當(dāng)前排隊(duì)的號(hào)碼
          public void unLock(int ticketNum){
          queueNum.compareAndSet(ticketNum,ticketNum + 1);
          }

          }

          每次叫號(hào)機(jī)在叫號(hào)的時(shí)候,都會(huì)判斷自己是不是被叫的號(hào),并且每個(gè)人在辦完業(yè)務(wù)的時(shí)候,叫號(hào)機(jī)根據(jù)在當(dāng)前號(hào)碼的基礎(chǔ)上 + 1,讓隊(duì)列繼續(xù)往前走。

          但是上面這個(gè)設(shè)計(jì)是有問題的,因?yàn)楂@得自己的號(hào)碼之后,是可以對(duì)號(hào)碼進(jìn)行更改的,這就造成系統(tǒng)紊亂,鎖不能及時(shí)釋放。這時(shí)候就需要有一個(gè)能確保每個(gè)人按會(huì)著自己號(hào)碼排隊(duì)辦業(yè)務(wù)的角色,在得知這一點(diǎn)之后,我們重新設(shè)計(jì)一下這個(gè)邏輯

          public class TicketLock2 {

          // 隊(duì)列票據(jù)(當(dāng)前排隊(duì)號(hào)碼)
          private AtomicInteger queueNum = new AtomicInteger();

          // 出隊(duì)票據(jù)(當(dāng)前需等待號(hào)碼)
          private AtomicInteger dueueNum = new AtomicInteger();

          private ThreadLocal ticketLocal = new ThreadLocal<>();

          public void lock(){
          int currentTicketNum = dueueNum.incrementAndGet();

          // 獲取鎖的時(shí)候,將當(dāng)前線程的排隊(duì)號(hào)保存起來
          ticketLocal.set(currentTicketNum);
          while (currentTicketNum != queueNum.get()){
          // doSomething...
          }
          }

          // 釋放鎖:從排隊(duì)緩沖池中取
          public void unLock(){
          Integer currentTicket = ticketLocal.get();
          queueNum.compareAndSet(currentTicket,currentTicket + 1);
          }

          }

          這次就不再需要返回值,辦業(yè)務(wù)的時(shí)候,要將當(dāng)前的這一個(gè)號(hào)碼緩存起來,在辦完業(yè)務(wù)后,需要釋放緩存的這條票據(jù)。

          缺點(diǎn)

          TicketLock 雖然解決了公平性的問題,但是多處理器系統(tǒng)上,每個(gè)進(jìn)程/線程占用的處理器都在讀寫同一個(gè)變量queueNum ,每次讀寫操作都必須在多個(gè)處理器緩存之間進(jìn)行緩存同步,這會(huì)導(dǎo)致繁重的系統(tǒng)總線和內(nèi)存的流量,大大降低系統(tǒng)整體的性能。

          為了解決這個(gè)問題,MCSLock 和 CLHLock 應(yīng)運(yùn)而生。

          CLHLock

          上面說到TicketLock 是基于隊(duì)列的,那么 CLHLock 就是基于鏈表設(shè)計(jì)的,CLH的發(fā)明人是:Craig,Landin and Hagersten,用它們各自的字母開頭命名。CLH 是一種基于鏈表的可擴(kuò)展,高性能,公平的自旋鎖,申請(qǐng)線程只能在本地變量上自旋,它會(huì)不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋。

          public class CLHLock {

          public static class CLHNode{
          private volatile boolean isLocked = true;
          }

          // 尾部節(jié)點(diǎn)
          private volatile CLHNode tail;
          private static final ThreadLocal LOCAL = new ThreadLocal<>();
          private static final AtomicReferenceFieldUpdater UPDATER =
          AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");


          public void lock(){
          // 新建節(jié)點(diǎn)并將節(jié)點(diǎn)與當(dāng)前線程保存起來
          CLHNode node = new CLHNode();
          LOCAL.set(node);

          // 將新建的節(jié)點(diǎn)設(shè)置為尾部節(jié)點(diǎn),并返回舊的節(jié)點(diǎn)(原子操作),這里舊的節(jié)點(diǎn)實(shí)際上就是當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)
          CLHNode preNode = UPDATER.getAndSet(this,node);
          if(preNode != null){
          // 前驅(qū)節(jié)點(diǎn)不為null表示當(dāng)鎖被其他線程占用,通過不斷輪詢判斷前驅(qū)節(jié)點(diǎn)的鎖標(biāo)志位等待前驅(qū)節(jié)點(diǎn)釋放鎖
          while (preNode.isLocked){

          }
          preNode = null;
          LOCAL.set(node);
          }
          // 如果不存在前驅(qū)節(jié)點(diǎn),表示該鎖沒有被其他線程占用,則當(dāng)前線程獲得鎖
          }

          public void unlock() {
          // 獲取當(dāng)前線程對(duì)應(yīng)的節(jié)點(diǎn)
          CLHNode node = LOCAL.get();
          // 如果tail節(jié)點(diǎn)等于node,則將tail節(jié)點(diǎn)更新為null,同時(shí)將node的lock狀態(tài)職位false,表示當(dāng)前線程釋放了鎖
          if (!UPDATER.compareAndSet(this, node, null)) {
          node.isLocked = false;
          }
          node = null;
          }
          }

          MCSLock

          MCS Spinlock 是一種基于鏈表的可擴(kuò)展、高性能、公平的自旋鎖,申請(qǐng)線程只在本地變量上自旋,直接前驅(qū)負(fù)責(zé)通知其結(jié)束自旋,從而極大地減少了不必要的處理器緩存同步的次數(shù),降低了總線和內(nèi)存的開銷。MCS 來自于其發(fā)明人名字的首字母:John Mellor-Crummey 和 Michael Scott。

          public class MCSLock {

          public static class MCSNode {
          volatile MCSNode next;
          volatile boolean isLocked = true;
          }

          private static final ThreadLocal NODE = new ThreadLocal<>();

          // 隊(duì)列
          @SuppressWarnings("unused")
          private volatile MCSNode queue;

          private static final AtomicReferenceFieldUpdater UPDATE =
          AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue");


          public void lock(){
          // 創(chuàng)建節(jié)點(diǎn)并保存到ThreadLocal中
          MCSNode currentNode = new MCSNode();
          NODE.set(currentNode);

          // 將queue設(shè)置為當(dāng)前節(jié)點(diǎn),并且返回之前的節(jié)點(diǎn)
          MCSNode preNode = UPDATE.getAndSet(this, currentNode);
          if (preNode != null) {
          // 如果之前節(jié)點(diǎn)不為null,表示鎖已經(jīng)被其他線程持有
          preNode.next = currentNode;
          // 循環(huán)判斷,直到當(dāng)前節(jié)點(diǎn)的鎖標(biāo)志位為false
          while (currentNode.isLocked) {
          }
          }
          }

          public void unlock() {
          MCSNode currentNode = NODE.get();
          // next為null表示沒有正在等待獲取鎖的線程
          if (currentNode.next == null) {
          // 更新狀態(tài)并設(shè)置queue為null
          if (UPDATE.compareAndSet(this, currentNode, null)) {
          // 如果成功了,表示queue==currentNode,即當(dāng)前節(jié)點(diǎn)后面沒有節(jié)點(diǎn)了
          return;
          } else {
          // 如果不成功,表示queue!=currentNode,即當(dāng)前節(jié)點(diǎn)后面多了一個(gè)節(jié)點(diǎn),表示有線程在等待
          // 如果當(dāng)前節(jié)點(diǎn)的后續(xù)節(jié)點(diǎn)為null,則需要等待其不為null(參考加鎖方法)
          while (currentNode.next == null) {
          }
          }
          } else {
          // 如果不為null,表示有線程在等待獲取鎖,此時(shí)將等待線程對(duì)應(yīng)的節(jié)點(diǎn)鎖狀態(tài)更新為false,同時(shí)將當(dāng)前線程的后繼節(jié)點(diǎn)設(shè)為null
          currentNode.next.isLocked = false;
          currentNode.next = null;
          }
          }
          }

          CLHLock 和 MCSLock

          • 都是基于鏈表,不同的是CLHLock是基于隱式鏈表,沒有真正的后續(xù)節(jié)點(diǎn)屬性,MCSLock是顯示鏈表,有一個(gè)指向后續(xù)節(jié)點(diǎn)的屬性。
          • 將獲取鎖的線程狀態(tài)借助節(jié)點(diǎn)(node)保存,每個(gè)線程都有一份獨(dú)立的節(jié)點(diǎn),這樣就解決了TicketLock多處理器緩存同步的問題。

          多個(gè)線程并發(fā)訪問資源

          鎖狀態(tài)的分類

          Java 語言專門針對(duì) synchronized 關(guān)鍵字設(shè)置了四種狀態(tài),它們分別是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖,但是在了解這些鎖之前還需要先了解一下 Java 對(duì)象頭和 Monitor。

          Java 對(duì)象頭

          我們知道 synchronized 是悲觀鎖,在操作同步之前需要給資源加鎖,這把鎖就是對(duì)象頭里面的,而Java 對(duì)象頭又是什么呢?我們以 Hotspot 虛擬機(jī)為例,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)class Pointer(類型指針)

          Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是說在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。

          class Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。

          在32位虛擬機(jī)和64位虛擬機(jī)的 Mark Word 所占用的字節(jié)大小不一樣,32位虛擬機(jī)的 Mark Word 和 class Pointer 分別占用 32bits 的字節(jié),而 64位虛擬機(jī)的 Mark Word 和 class Pointer 占用了64bits 的字節(jié),下面我們以 32位虛擬機(jī)為例,來看一下其 Mark Word 的字節(jié)具體是如何分配的


          34fe01e458bb75bd280fd0c90bce037c.webp


          ea471d5544b13f930299608a5c8a9ed0.webp

          用中文翻譯過來就是

          7a1cca248ac93074d1fd3f89c8337263.webp

          • 無狀態(tài)也就是無鎖的時(shí)候,對(duì)象頭開辟 25bit 的空間用來存儲(chǔ)對(duì)象的 hashcode ,4bit 用于存放分代年齡,1bit 用來存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來存放鎖標(biāo)識(shí)位為01
          • 偏向鎖 中劃分更細(xì),還是開辟25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無鎖,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01
          • 輕量級(jí)鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00
          • 重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來存放指向重量級(jí)鎖的指針,2bit 存放鎖的標(biāo)識(shí)位,為11
          • GC標(biāo)記開辟30bit 的內(nèi)存空間卻沒有占用,2bit 空間存放鎖標(biāo)志位為11。

          其中無鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無鎖狀態(tài)還是偏向鎖狀態(tài)。

          關(guān)于為什么這么分配的內(nèi)存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪

          57f04b43473547b25369254245819df6.webp

          來解釋一下

          • age_bits 就是我們說的分代回收的標(biāo)識(shí),占用4字節(jié)
          • lock_bits 是鎖的標(biāo)志位,占用2個(gè)字節(jié)
          • biased_lock_bits 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)
          • max_hash_bits 是針對(duì)無鎖計(jì)算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte
          • hash_bits 是針對(duì) 64 位虛擬機(jī)來說,如果最大字節(jié)數(shù)大于 31,則取31,否則取真實(shí)的字節(jié)數(shù)
          • cms_bits 我覺得應(yīng)該是不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte
          • epoch_bits 就是 epoch 所占用的字節(jié)大小,2字節(jié)。

          Synchronized鎖

          synchronized用的鎖是存在Java對(duì)象頭里的。

          JVM基于進(jìn)入和退出 Monitor 對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實(shí)現(xiàn)的,monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結(jié)束處和異常處。任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)且一個(gè) monitor 被持有后,它將處于鎖定狀態(tài)。

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

          Monitor

          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ì)比較長(zhǎng)的時(shí)間,這就是為什么 Synchronized 效率低的原因。因此,這種依賴于操作系統(tǒng) Mutex Lock 所實(shí)現(xiàn)的鎖我們稱之為重量級(jí)鎖

          Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖輕量級(jí)鎖:鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖可以升級(jí)但不能降級(jí)。

          所以鎖的狀態(tài)總共有四種:無鎖狀態(tài)、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖。隨著鎖的競(jìng)爭(zhēng),鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖(但是鎖的升級(jí)是單向的,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí))。JDK 1.6中默認(rèn)是開啟偏向鎖和輕量級(jí)鎖的,我們也可以通過-XX:-UseBiasedLocking=false來禁用偏向鎖。

          鎖的分類及其解釋

          先來個(gè)大體的流程圖來感受一下這個(gè)過程,然后下面我們?cè)俜珠_來說


          0f98d7d6e67e19e9d7802f6ef29fc65c.webp

          無鎖

          無鎖狀態(tài),無鎖即沒有對(duì)資源進(jìn)行鎖定,所有的線程都可以對(duì)同一個(gè)資源進(jìn)行訪問,但是只有一個(gè)線程能夠成功修改資源。

          7ba36d14cf0e011493f4300e573cde19.webp

          無鎖的特點(diǎn)就是在循環(huán)內(nèi)進(jìn)行修改操作,線程會(huì)不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現(xiàn)沖突的發(fā)生,這很像我們?cè)谥拔恼轮薪榻B的 CAS 實(shí)現(xiàn),CAS 的原理和應(yīng)用就是無鎖的實(shí)現(xiàn)。無鎖無法全面代替有鎖,但無鎖在某些場(chǎng)合下的性能是非常高的。

          偏向鎖

          HotSpot 的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個(gè)線程執(zhí)行同步時(shí)提高性能。

          12f46697757ddac1c1eb128d736d0331.webp

          可以從對(duì)象頭的分配中看到,偏向鎖要比無鎖多了線程IDepoch,下面我們就來描述一下偏向鎖的獲取過程

          偏向鎖獲取過程

          1. 首先線程訪問同步代碼塊,會(huì)通過檢查對(duì)象頭 Mark Word 的鎖標(biāo)志位判斷目前鎖的狀態(tài),如果是 01,說明就是無鎖或者偏向鎖,然后再根據(jù)是否偏向鎖 的標(biāo)示判斷是無鎖還是偏向鎖,如果是無鎖情況下,執(zhí)行下一步
          2. 線程使用 CAS 操作來嘗試對(duì)對(duì)象加鎖,如果使用 CAS 替換 ThreadID 成功,就說明是第一次上鎖,那么當(dāng)前線程就會(huì)獲得對(duì)象的偏向鎖,此時(shí)會(huì)在對(duì)象頭的 Mark Word 中記錄當(dāng)前線程 ID 和獲取鎖的時(shí)間 epoch 等信息,然后執(zhí)行同步代碼塊。

          全局安全點(diǎn)(Safe Point):全局安全點(diǎn)的理解會(huì)涉及到 C 語言底層的一些知識(shí),這里簡(jiǎn)單理解 SafePoint 是 Java 代碼中的一個(gè)線程可能暫停執(zhí)行的位置。

          等到下一次線程在進(jìn)入和退出同步代碼塊時(shí)就不需要進(jìn)行 CAS 操作進(jìn)行加鎖和解鎖,只需要簡(jiǎn)單判斷一下對(duì)象頭的 Mark Word 中是否存儲(chǔ)著指向當(dāng)前線程的線程ID,判斷的標(biāo)志當(dāng)然是根據(jù)鎖的標(biāo)志位來判斷的。如果用流程圖來表示的話就是下面這樣


          427de9eebc10b459c359f10fe9c38c49.webp

          關(guān)閉偏向鎖

          偏向鎖在Java 6 和Java 7 里是默認(rèn)啟用的。由于偏向鎖是為了在只有一個(gè)線程執(zhí)行同步塊時(shí)提高性能,如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。

          關(guān)于 epoch

          偏向鎖的對(duì)象頭中有一個(gè)被稱為 epoch 的值,它作為偏差有效性的時(shí)間戳。

          輕量級(jí)鎖

          輕量級(jí)鎖是指當(dāng)前鎖是偏向鎖的時(shí)候,資源被另外的線程所訪問,那么偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能,下面是詳細(xì)的獲取過程。

          輕量級(jí)鎖加鎖過程

          1. 緊接著上一步,如果 CAS 操作替換 ThreadID 沒有獲取成功,執(zhí)行下一步
          2. 如果使用 CAS 操作替換 ThreadID 失敗(這時(shí)候就切換到另外一個(gè)線程的角度)說明該資源已被同步訪問過,這時(shí)候就會(huì)執(zhí)行鎖的撤銷操作,撤銷偏向鎖,然后等原持有偏向鎖的線程到達(dá)全局安全點(diǎn)(SafePoint)時(shí),會(huì)暫停原持有偏向鎖的線程,然后會(huì)檢查原持有偏向鎖的狀態(tài),如果已經(jīng)退出同步,就會(huì)喚醒持有偏向鎖的線程,執(zhí)行下一步
          3. 檢查對(duì)象頭中的 Mark Word 記錄的是否是當(dāng)前線程 ID,如果是,執(zhí)行同步代碼,如果不是,執(zhí)行偏向鎖獲取流程 的第2步。

          如果用流程表示的話就是下面這樣(已經(jīng)包含偏向鎖的獲取)


          2bd3300af3a694e54e1ca39e362e7078.webp

          重量級(jí)鎖

          重量級(jí)鎖的獲取流程比較復(fù)雜,小伙伴們做好準(zhǔn)備,其實(shí)多看幾遍也沒那么麻煩,呵呵。

          重量級(jí)鎖的獲取流程

          1.?接著上面偏向鎖的獲取過程,由偏向鎖升級(jí)為輕量級(jí)鎖,執(zhí)行下一步

          2 .?會(huì)在原持有偏向鎖的線程的棧中分配鎖記錄,將對(duì)象頭中的 Mark Word 拷貝到原持有偏向鎖線程的記錄中,然后原持有偏向鎖的線程獲得輕量級(jí)鎖,然后喚醒原持有偏向鎖的線程,從安全點(diǎn)處繼續(xù)執(zhí)行,執(zhí)行完畢后,執(zhí)行下一步,當(dāng)前線程執(zhí)行第4步

          3. 執(zhí)行完畢后,開始輕量級(jí)解鎖操作,解鎖需要判斷兩個(gè)條件

          ????????如果上面兩個(gè)判斷條件都符合的話,就進(jìn)行鎖釋放,如果其中一個(gè)條件不?符合,就會(huì)釋放鎖,并喚起等待的線程,進(jìn)行新一輪的鎖競(jìng)爭(zhēng)。

          8599de43715b46d18e064dcaaca0a2ac.webp

          拷貝在當(dāng)前線程鎖記錄的 Mark Word 信息是否與對(duì)象頭中的 Mark Word 一致。判斷對(duì)象頭中的 Mark Word 中鎖記錄指針是否指向當(dāng)前棧中記錄的指針

          4. 在當(dāng)前線程的棧中分配鎖記錄,拷貝對(duì)象頭中的 MarkWord 到當(dāng)前線程的鎖記錄中,執(zhí)行 CAS 加鎖操作,會(huì)把對(duì)象頭 Mark Word 中鎖記錄指針指向當(dāng)前線程鎖記錄,如果成功,獲取輕量級(jí)鎖,執(zhí)行同步代碼,然后執(zhí)行第3步,如果不成功,執(zhí)行下一步

          5.?當(dāng)前線程沒有使用 CAS 成功獲取鎖,就會(huì)自旋一會(huì)兒,再次嘗試獲取,如果在多次自旋到達(dá)上限后還沒有獲取到鎖,那么輕量級(jí)鎖就會(huì)升級(jí)為 重量級(jí)鎖

          0c51c82e5de25116184c99d79930beda.webp

          如果用流程圖表示是這樣的


          b4e56c4aa99deda50125342528b7f220.webp

          鎖的公平性與非公平性

          我們知道,在并發(fā)環(huán)境中,多個(gè)線程需要對(duì)同一資源進(jìn)行訪問,同一時(shí)刻只能有一個(gè)線程能夠獲取到鎖并進(jìn)行資源訪問,那么剩下的這些線程怎么辦呢?這就好比食堂排隊(duì)打飯的模型,最先到達(dá)食堂的人擁有最先買飯的權(quán)利,那么剩下的人就需要在第一個(gè)人后面排隊(duì),這是理想的情況,即每個(gè)人都能夠買上飯。那么現(xiàn)實(shí)情況是,在你排隊(duì)的過程中,就有個(gè)別不老實(shí)的人想走捷徑,插隊(duì)打飯,如果插隊(duì)的這個(gè)人后面沒有人制止他這種行為,他就能夠順利買上飯,如果有人制止,他就也得去隊(duì)伍后面排隊(duì)。

          對(duì)于正常排隊(duì)的人來說,沒有人插隊(duì),每個(gè)人都在等待排隊(duì)打飯的機(jī)會(huì),那么這種方式對(duì)每個(gè)人來說都是公平的,先來后到嘛。這種鎖也叫做公平鎖。


          980cde2a94d06a79159ff235be89bc72.webp

          那么假如插隊(duì)的這個(gè)人成功買上飯并且在買飯的過程不管有沒有人制止他,他的這種行為對(duì)正常排隊(duì)的人來說都是不公平的,這在鎖的世界中也叫做非公平鎖。


          f9a4c14480f99a2abcab15be964cfdd4.webp


          26f6727542cb9fd1f7849c4c091bf1b3.webp


          那么我們根據(jù)上面的描述可以得出下面的結(jié)論

          公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進(jìn)先出順序。而非公平鎖就是一種獲取鎖的搶占機(jī)制,是隨機(jī)獲得鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這個(gè)方式可能造成某些線程一直拿不到鎖,結(jié)果也就是不公平的了。

          鎖公平性的實(shí)現(xiàn)

          在 Java 中,我們一般通過 ReetrantLock 來實(shí)現(xiàn)鎖的公平性

          我們分別通過兩個(gè)例子來講解一下鎖的公平性和非公平性

          鎖的公平性

          public class MyFairLock extends Thread{

          private ReentrantLock lock = new ReentrantLock(true);
          public void fairLock(){
          try {
          lock.lock();
          System.out.println(Thread.currentThread().getName() + "正在持有鎖");
          }finally {
          System.out.println(Thread.currentThread().getName() + "釋放了鎖");
          lock.unlock();
          }
          }

          public static void main(String[] args) {
          MyFairLock myFairLock = new MyFairLock();
          Runnable runnable = () -> {
          System.out.println(Thread.currentThread().getName() + "啟動(dòng)");
          myFairLock.fairLock();
          };
          Thread[] thread = new Thread[10];
          for(int i = 0;i < 10;i++){
          thread[i] = new Thread(runnable);
          }
          for(int i = 0;i < 10;i++){
          thread[i].start();
          }
          }
          }

          我們創(chuàng)建了一個(gè) ReetrantLock,并給構(gòu)造函數(shù)傳了一個(gè) true,我們可以查看 ReetrantLock 的構(gòu)造函數(shù)

          public ReentrantLock(boolean fair) {
          sync = fair ? new FairSync() : new NonfairSync();
          }

          根據(jù) JavaDoc 的注釋可知,如果是 true 的話,那么就會(huì)創(chuàng)建一個(gè) ReentrantLock 的公平鎖,然后并創(chuàng)建一個(gè) FairSync ,F(xiàn)airSync 其實(shí)是一個(gè) Sync 的內(nèi)部類,它的主要作用是同步對(duì)象以獲取公平鎖。

          087a5bc19767377b54cfced111ff5dd7.webp

          而 Sync 是 ReentrantLock 中的內(nèi)部類,Sync 繼承 AbstractQueuedSynchronizer 類,AbstractQueuedSynchronizer 就是我們常說的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一個(gè)類,通過它來實(shí)現(xiàn)獨(dú)占鎖和共享鎖。

          abstract static class Sync extends AbstractQueuedSynchronizer {...}

          也就是說,我們把 fair 參數(shù)設(shè)置為 true 之后,就可以實(shí)現(xiàn)一個(gè)公平鎖了,是這樣嗎?我們回到示例代碼,我們可以執(zhí)行一下這段代碼,它的輸出是順序獲取的(礙于篇幅的原因,這里就暫不貼出了),也就是說我們創(chuàng)建了一個(gè)公平鎖

          鎖的非公平性

          與公平性相對(duì)的就是非公平性,我們通過設(shè)置 fair 參數(shù)為 true,便實(shí)現(xiàn)了一個(gè)公平鎖,與之相對(duì)的,我們把 fair 參數(shù)設(shè)置為 false,是不是就是非公平鎖了?用事實(shí)證明一下

          private ReentrantLock lock = new ReentrantLock(false);

          其他代碼不變,我們執(zhí)行一下看看輸出(部分輸出)

          Thread-1啟動(dòng)
          Thread-4啟動(dòng)
          Thread-1正在持有鎖
          Thread-1釋放了鎖
          Thread-5啟動(dòng)
          Thread-6啟動(dòng)
          Thread-3啟動(dòng)
          Thread-7啟動(dòng)
          Thread-2啟動(dòng)

          可以看到,線程的啟動(dòng)并沒有按順序獲取,可以看出非公平鎖對(duì)鎖的獲取是亂序的,即有一個(gè)搶占鎖的過程。也就是說,我們把 fair 參數(shù)設(shè)置為 false 便實(shí)現(xiàn)了一個(gè)非公平鎖。

          ReentrantLock 基本概述

          ReentrantLock 是一把可重入鎖,也是一把互斥鎖,它具有與 synchronized 相同的方法和監(jiān)視器鎖的語義,但是它比 synchronized 有更多可擴(kuò)展的功能。

          ReentrantLock 的可重入性是指它可以由上次成功鎖定但還未解鎖的線程擁有。當(dāng)只有一個(gè)線程嘗試加鎖時(shí),該線程調(diào)用 lock() 方法會(huì)立刻返回成功并直接獲取鎖。如果當(dāng)前線程已經(jīng)擁有這把鎖,這個(gè)方法會(huì)立刻返回。可以使用 isHeldByCurrentThreadgetHoldCount 進(jìn)行檢查。

          這個(gè)類的構(gòu)造函數(shù)接受可選擇的 fairness 參數(shù),當(dāng) fairness 設(shè)置為 true 時(shí),在多線程爭(zhēng)奪嘗試加鎖時(shí),鎖傾向于對(duì)等待時(shí)間最長(zhǎng)的線程訪問,這也是公平性的一種體現(xiàn)。否則,鎖不能保證每個(gè)線程的訪問順序,也就是非公平鎖。與使用默認(rèn)設(shè)置的程序相比,使用許多線程訪問的公平鎖的程序可能會(huì)顯示較低的總體吞吐量(即較慢;通常要慢得多)。但是獲取鎖并保證線程不會(huì)饑餓的次數(shù)比較小。無論如何請(qǐng)注意:鎖的公平性不能保證線程調(diào)度的公平性。因此,使用公平鎖的多線程之一可能會(huì)連續(xù)多次獲得它,而其他活動(dòng)線程沒有進(jìn)行且當(dāng)前未持有該鎖。這也是互斥性 的一種體現(xiàn)。

          也要注意的 tryLock() 方法不支持公平性。如果鎖是可以獲取的,那么即使其他線程等待,它仍然能夠返回成功。

          推薦使用下面的代碼來進(jìn)行加鎖和解鎖

          class MyFairLock {
          private final ReentrantLock lock = new ReentrantLock();

          public void m() {
          lock.lock();
          try {
          // ...
          } finally {
          lock.unlock()
          }
          }
          }

          ReentrantLock 鎖通過同一線程最多支持2147483647個(gè)遞歸鎖。嘗試超過此限制會(huì)導(dǎo)致鎖定方法引發(fā)錯(cuò)誤。

          ReentrantLock 如何實(shí)現(xiàn)鎖公平性

          我們?cè)谏厦娴暮?jiǎn)述中提到,ReentrantLock 是可以實(shí)現(xiàn)鎖的公平性的,那么原理是什么呢?下面我們通過其源碼來了解一下 ReentrantLock 是如何實(shí)現(xiàn)鎖的公平性的

          跟蹤其源碼發(fā)現(xiàn),調(diào)用 Lock.lock() 方法其實(shí)是調(diào)用了 sync 的內(nèi)部的方法

          abstract void lock();

          而 sync 是最基礎(chǔ)的同步控制 Lock 的類,它有公平鎖和非公平鎖的實(shí)現(xiàn)。它繼承 AbstractQueuedSynchronizer 即 使用 AQS 狀態(tài)代表鎖持有的數(shù)量。

          lock 是抽象方法是需要被子類實(shí)現(xiàn)的,而繼承了 AQS 的類主要有

          f3f2d31e45fa835a89da3f361ecc848f.webp

          我們可以看到,所有實(shí)現(xiàn)了 AQS 的類都位于 JUC 包下,主要有五類:ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatchThreadPoolExecutor,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以實(shí)現(xiàn)公平鎖和非公平鎖。

          下面是公平鎖 FairSync 的繼承關(guān)系

          44dc3842ffb8e89adbdb6e78828f5e16.webp

          非公平鎖的NonFairSync 的繼承關(guān)系

          6cf96c7979178c5766bc17f98be5e5f0.webp

          由繼承圖可以看到,兩個(gè)類的繼承關(guān)系都是相同的,我們從源碼發(fā)現(xiàn),公平鎖和非公平鎖的實(shí)現(xiàn)就是下面這段代碼的區(qū)別(下一篇文章我們會(huì)從原理角度分析一下公平鎖和非公平鎖的實(shí)現(xiàn))

          770f62bb67af741c23bc871f0469533f.webp

          通過上圖中的源代碼對(duì)比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區(qū)別就在于公平鎖在獲取同步狀態(tài)時(shí)多了一個(gè)限制條件:hasQueuedPredecessors()

          hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用來 查詢是否有任何線程在等待獲取鎖的時(shí)間比當(dāng)前線程長(zhǎng),也就是說每個(gè)等待線程都是在一個(gè)隊(duì)列中,此方法就是判斷隊(duì)列中在當(dāng)前線程獲取鎖時(shí),是否有等待鎖時(shí)間比自己還長(zhǎng)的隊(duì)列,如果當(dāng)前線程之前有排隊(duì)的線程,返回 true,如果當(dāng)前線程位于隊(duì)列的開頭或隊(duì)列為空,返回 false。

          綜上,公平鎖就是通過同步隊(duì)列來實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖,從而實(shí)現(xiàn)公平的特性。非公平鎖加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖,所以存在后申請(qǐng)卻先獲得鎖的情況。

          根據(jù)鎖是否可重入進(jìn)行區(qū)分

          可重入鎖

          可重入鎖又稱為遞歸鎖,是指在同一個(gè)線程在外層方法獲取鎖的時(shí)候,再進(jìn)入該線程的內(nèi)層方法會(huì)自動(dòng)獲取鎖(前提鎖對(duì)象得是同一個(gè)對(duì)象或者class),不會(huì)因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞。Java 中 ReentrantLocksynchronized 都是可重入鎖,可重入鎖的一個(gè)優(yōu)點(diǎn)是在一定程度上可以避免死鎖。

          我們先來看一段代碼來說明一下 synchronized 的可重入性

          private synchronized void doSomething(){
          System.out.println("doSomething...");
          doSomethingElse();
          }

          private synchronized void doSomethingElse(){
          System.out.println("doSomethingElse...");
          }

          在上面這段代碼中,我們對(duì) doSomething()doSomethingElse() 分別使用了 synchronized 進(jìn)行鎖定,doSomething() 方法中調(diào)用了 doSomethingElse() 方法,因?yàn)?synchronized 是可重入鎖,所以同一個(gè)線程在調(diào)用 doSomething() 方法時(shí),也能夠進(jìn)入 doSomethingElse() 方法中。

          不可重入鎖

          如果 synchronized 是不可重入鎖的話,那么在調(diào)用 doSomethingElse() 方法的時(shí)候,必須把 doSomething() 的鎖丟掉,實(shí)際上該對(duì)象鎖已被當(dāng)前線程所持有,且無法釋放。所以此時(shí)會(huì)出現(xiàn)死鎖。

          也就是說,不可重入鎖會(huì)造成死鎖

          多個(gè)線程能夠共享同一把鎖

          獨(dú)占鎖和共享鎖

          獨(dú)占多和共享鎖一般對(duì)應(yīng) JDK 源碼的 ReentrantLock 和 ReentrantReadWriteLock 源碼來介紹獨(dú)占鎖和共享鎖。

          獨(dú)占鎖又叫做排他鎖,是指鎖在同一時(shí)刻只能被一個(gè)線程擁有,其他線程想要訪問資源,就會(huì)被阻塞。JDK 中 synchronized和 JUC 中 Lock 的實(shí)現(xiàn)類就是互斥鎖。

          共享鎖指的是鎖能夠被多個(gè)線程所擁有,如果某個(gè)線程對(duì)資源加上共享鎖后,則其他線程只能對(duì)資源再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)

          669d9ce9a28798054548c9a4fd037a04.webp

          我們看到 ReentrantReadWriteLock 有兩把鎖:ReadLockWriteLock,也就是一個(gè)讀鎖一個(gè)寫鎖,合在一起叫做讀寫鎖。再進(jìn)一步觀察可以發(fā)現(xiàn) ReadLock 和 WriteLock 是靠?jī)?nèi)部類 Sync 實(shí)現(xiàn)的鎖。Sync 是繼承于 AQS 子類的,AQS 是并發(fā)的根本,這種結(jié)構(gòu)在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

          在 ReentrantReadWriteLock 里面,讀鎖和寫鎖的鎖主體都是 Sync,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨(dú)享鎖。讀鎖的共享鎖可保證并發(fā)讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因?yàn)樽x鎖和寫鎖是分離的。所以ReentrantReadWriteLock的并發(fā)性相比一般的互斥鎖有了很大提升。

          其他優(yōu)質(zhì)并發(fā)文章推薦

          1、一句話擼完重量級(jí)鎖、自旋鎖、輕量級(jí)鎖、偏向鎖、悲觀、樂觀鎖等各種鎖
          2、線程安全(上)--徹底搞懂volatile關(guān)鍵字
          3、線程安全(下)--徹底搞懂synchronized(從偏向鎖到重量級(jí)鎖)

          另外,我正在整理一份計(jì)算機(jī)類書單,只為讓大家更加方便找到自己想要的書籍,目前已經(jīng)收集了幾百本了,貢獻(xiàn)給需要的人,目前是整理到了 github 上,方便更新,地址:https://github.com/iamshuaidi/CS-Book(點(diǎn)擊左下角的閱讀原文,可直達(dá))。

          截圖了部分目錄如下:

          9a2de4928d38e225a4072a5866f461a1.webp

          更多算法 + 計(jì)算機(jī)基礎(chǔ)知識(shí)(計(jì)算機(jī)網(wǎng)絡(luò)+ 操作系統(tǒng)+數(shù)據(jù)庫(kù)+Linux等)文章,歡迎更多我的微信公眾號(hào)哦。

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人特级毛片全部免费播放 | 亚洲黄色影院 | 操逼操逼片 | 欧美精品三级在线观看 | WWW.日韩AV电影 |