Java常用鎖

1. 并發(fā)競(jìng)爭(zhēng)概述

竟態(tài)條件:多線程在臨界區(qū)執(zhí)行,由于代碼執(zhí)行序列不可預(yù)知而導(dǎo)致無(wú)法預(yù)測(cè)結(jié)果
解決方式:
(1)阻塞式:sync, Lock(ReentrantLock)
(2)非阻塞式:Cas方式(自旋式)

2. Synchronized

2.1 使用方法

(1)實(shí)例方法加鎖:鎖住實(shí)例對(duì)象

public class MyClass {
    private int count;

    public synchronized void increment() {
        count++;
    }
}

在實(shí)例化對(duì)象上進(jìn)行鎖的判斷
(2)靜態(tài)方法加鎖:鎖住類(lèi)對(duì)象

public class MyClass {
    private static int count;

    public static synchronized void increment() {
        count++;
    }
}

在靜態(tài)類(lèi)MyClass上進(jìn)行鎖的判斷
(3)加鎖在代碼塊上:

public class MyClass {
    private int count;
    private final Object lock = new Object();
    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

我們創(chuàng)建了一個(gè)名為 lock 的對(duì)象,并在每個(gè)方法中使用 synchronized 關(guān)鍵字來(lái)獲取這個(gè)對(duì)象的鎖。當(dāng)一個(gè)線程進(jìn)入 synchronized 代碼塊時(shí),它會(huì)獲取 lock 對(duì)象的鎖,然后執(zhí)行代碼塊中的代碼。其他線程如果想要進(jìn)入這個(gè)代碼塊,必須等待當(dāng)前線程釋放鎖之后才能獲取鎖并執(zhí)行代碼塊中的代碼。

2.2 synchronized鎖競(jìng)爭(zhēng)

sync工作流程.png

2.3 synchronized加鎖原理

(1)早期實(shí)現(xiàn):sync為JVM內(nèi)置鎖,(JVM層面)基于Monitor(監(jiān)視器)機(jī)制實(shí)現(xiàn),(Linux層面)依賴底層操作系統(tǒng)的Mutux(互斥量)實(shí)現(xiàn),由于進(jìn)行了內(nèi)核態(tài)及用戶態(tài)的切換,性能較低
(2)優(yōu)化后:通過(guò)鎖升級(jí)實(shí)現(xiàn)加鎖過(guò)程:偏向鎖,自旋鎖,輕量級(jí),重量級(jí)
(3)monitor(監(jiān)視器)原理:
同步方法由Au_SYNCHRONIZED標(biāo)志實(shí)現(xiàn), 同步代碼塊由monitorenter與monitorexit實(shí)現(xiàn);
是管理共享變量以及對(duì)共享變量操作的過(guò)程,讓它們支持并發(fā);
synchronized中wait(), notify(), notifyAll()等方法有monitor實(shí)現(xiàn);
補(bǔ)充:
java線程等待方法:sleep, wait, join, unpark

名稱(chēng) 作用 特點(diǎn)
sleep 讓當(dāng)前線程暫停執(zhí)行一段時(shí)間,再繼續(xù)執(zhí)行 Thread 類(lèi)的靜態(tài)方法線程不會(huì)釋放鎖,其他線程無(wú)法訪問(wèn)被鎖定的資源
wait 讓當(dāng)前線程暫停執(zhí)行,直到其他線程調(diào)用該對(duì)象的 notify() 或 notifyAll() 方法喚醒它 在調(diào)用 wait() 方法時(shí),線程會(huì)釋放鎖,其他線程可以訪問(wèn)被鎖定的資源。wait() 方法必須在 synchronized 塊中調(diào)用,否則會(huì)拋出 IllegalMonitorStateException 異常
join 當(dāng)前線程會(huì)暫停執(zhí)行,直到另一個(gè)線程執(zhí)行完畢 Thread 類(lèi)的實(shí)例方法,可以讓當(dāng)前線程等待另一個(gè)線程執(zhí)行完畢。在調(diào)用 join() 方法時(shí),當(dāng)前線程會(huì)暫停執(zhí)行,直到另一個(gè)線程執(zhí)行完畢。
LockSupport.park() LockSupport 類(lèi)的靜態(tài)方法,可以讓當(dāng)前線程暫停執(zhí)行,直到其他線程調(diào)用該線程的 unpark() 方法喚醒它 LockSupport.park() 方法可以用于線程的阻塞和喚醒,例如等待某個(gè)條件的滿足后再繼續(xù)執(zhí)行

注:sleep() 方法和 wait() 方法可以在任何地方調(diào)用,而 join() 方法和 LockSupport.park() 方法必須在線程內(nèi)部調(diào)用。sleep() 方法和 wait() 方法可以讓線程等待一段時(shí)間后繼續(xù)執(zhí)行,而 join() 方法和 LockSupport.park() 方法可以讓線程等待其他線程的執(zhí)行或喚醒。

(4)sync加鎖解鎖流程圖:

(5)對(duì)象內(nèi)存布局


對(duì)象內(nèi)存布局.png

2.4 synchronized鎖升級(jí)

2.4.1 偏向鎖:

(1)流程:程序啟動(dòng)時(shí),將對(duì)象的標(biāo)記設(shè)置為偏向鎖,表示該對(duì)象目前沒(méi)有競(jìng)爭(zhēng),可以被當(dāng)前線程獨(dú)占;
當(dāng)其他線程訪問(wèn)該對(duì)象時(shí),會(huì)檢查該對(duì)象的標(biāo)記,如果是偏向鎖,則會(huì)判斷當(dāng)前線程是否是偏向鎖的擁有者,如果是,則直接獲取鎖,否則會(huì)升級(jí)為輕量級(jí)鎖或重量級(jí)鎖;
優(yōu)點(diǎn):減少鎖競(jìng)爭(zhēng)
(2)撤銷(xiāo)條件:
obj.wait() 偏向鎖撤銷(xiāo),升級(jí)為重量級(jí)鎖
notify(), notifyAll() 升級(jí)為輕量級(jí)鎖

2.4.2 鎖升級(jí)過(guò)程:

(1)在偏向鎖的基礎(chǔ)上,有線程P2加入競(jìng)爭(zhēng),會(huì)檢查該對(duì)象的標(biāo)記,如果當(dāng)前線程不是偏向鎖的擁有者,則會(huì)升級(jí)為輕量級(jí)鎖,輕量級(jí)鎖(又稱(chēng)自旋鎖)的功能是讓P2線程不斷自旋(while + cas)
注:若線程P1此時(shí)sleep,無(wú)競(jìng)爭(zhēng),則仍為偏向鎖
(2)線程自旋達(dá)到一定次數(shù)失敗,進(jìn)行線程阻塞(切換到內(nèi)核態(tài)),此時(shí)為重量級(jí)鎖;
重量級(jí)鎖是一種基于操作系統(tǒng)的鎖,它的作用是在獲取鎖時(shí),將當(dāng)前線程掛起,等待鎖的擁有者釋放鎖。重量級(jí)鎖的效率比較低,因?yàn)樗枰M(jìn)行線程的上下文切換和內(nèi)核態(tài)和用戶態(tài)之間的切換。

3. ReentrantLock原理

3.1 操作系統(tǒng)中的并發(fā)處理--管程

管程概念.png

3.2 JVM層面對(duì)管程的實(shí)現(xiàn)

synchronized:
objectMonitor & cxq(cas owner)-> 等效于同步等待隊(duì)列
waitset -> 等效于條件等待隊(duì)列

3.3 java中管程實(shí)現(xiàn)

(1)抽象層面:
同步等待隊(duì)列實(shí)現(xiàn):CAS機(jī)制, volatile int state,等待隊(duì)列
條件等待隊(duì)列實(shí)現(xiàn):conditional await, signal(), signalAll(), 出隊(duì)入隊(duì)條件
(2)工具層面:
抽象隊(duì)列同步器AQS

3.4 AQS原理

java.util.concurrent包中年基礎(chǔ)能力組件,常用于實(shí)現(xiàn)依賴狀態(tài)同步器

3.4.1 特性

(1)阻塞等待隊(duì)列:由條件隊(duì)列,同步隊(duì)列共同實(shí)現(xiàn)
(2)共享/獨(dú)占鎖:共享鎖:semaphore(信號(hào)量), CountDownLatch
獨(dú)占鎖:ReentrantLock
(3)公平/非公平鎖
(4)是否可重入:state表示狀態(tài)
(5)是否允許中斷:設(shè)置中斷標(biāo)志位

3.4.2 兩種等待隊(duì)列

(1)同步等待隊(duì)列:維護(hù)獲取資源(鎖)失敗后的線程
(2)條件等待隊(duì)列:
調(diào)用await()時(shí)釋放鎖,線程加入條件隊(duì)列
signal()時(shí)喚醒條件隊(duì)列中的線程,加入同步等待隊(duì)列中,等待獲取


等待隊(duì)列.png

3.4.3 ReentrantLock應(yīng)用

解決庫(kù)存超賣(mài)問(wèn)題

public string reduceStock() {
  int stock = Integer.parseInt(StringRedisTemplate.OPsForValue().get("stock"));
  if (stock > 0) {
    stock--;
    StringRedisTemplate.opsForValue().set("stock", stock);
  }
  return "end";
}

在多線程下會(huì)造成stock超賣(mài)

3.4.4 小總結(jié)

解決并發(fā)問(wèn)題
(1)非阻塞式:cas + 自旋鎖
(2)阻塞式:synchronized, ReentrantLock
公平鎖/非公平鎖區(qū)別:
(1)非公平鎖效率更高(對(duì)CPU的使用效率高);不用經(jīng)歷從等待隊(duì)列喚醒線程的步驟,有新的任務(wù)嘗試獲取鎖資源即可成功
(2)公平鎖保證線程不會(huì)長(zhǎng)時(shí)間陷于饑餓狀態(tài)

3.4.5 條件隊(duì)列的使用場(chǎng)景

boolean volatile hashcig;
public static Conditional cond = lock.newCondition();
public static cigratte() {
  lock.lock();
  try {
    while (!hashcig) {
      try {
        log("no cigurate, wait");
        cond.await();
      } catch(Exception e) e.print();
    } 
   log("begin work);
  finally {
      lock.unlock();
    }
  }
}

喚醒邏輯
new Thread(() -> {
  lock.lock();
  try {
    hashcig = true;
    cond.signal("上煙了");
  } ...
}

3.4.6 流程圖

4. AQS框架擴(kuò)展

4.1 semaphore信號(hào)量

(1)作用:實(shí)現(xiàn)互斥鎖,通過(guò)同時(shí)只能有一個(gè)線程能獲取信息量;用于實(shí)現(xiàn)限流功能;
(2)工作原理:設(shè)置窗口值,當(dāng)未達(dá)到該窗口值時(shí),工作線程正常工作;當(dāng)窗口值為0時(shí),不支持新的線程執(zhí)行任務(wù),新的線程阻塞,進(jìn)入阻塞隊(duì)列;當(dāng)上一批線程釋放資源后,到等待隊(duì)列中喚醒等待線程,重新執(zhí)行以上流程;
注:即根據(jù)窗口值一個(gè)批次一個(gè)批次執(zhí)行多線程任務(wù),上個(gè)批次執(zhí)行時(shí)窗口值為0,則后面的線程阻塞;


信號(hào)量流程.png

(3)舉例

semphore windows = new semphore(3); // 設(shè)置窗口值
for (;;) {
  new Thread(new Runnable() {
      public void run() {
        try {
        windows.acquire();  // 占用窗口, windows - 1
        Thread.sleep();
      } finally {
        window.release();  // 釋放
      }
    }
  }
}

4.2 CountDownLatch

(1)作用:是一個(gè)同步協(xié)作類(lèi),允許一個(gè)或多個(gè)線程等待,直到其他線程完成操作集;使用給定的count初始化,await后阻塞直到當(dāng)前計(jì)數(shù)值;由于countDown方法調(diào)用達(dá)到0,count為0后所有等待線程均釋放;
注:將所有線程阻塞,直到countDown減到0值,調(diào)用unpark喚醒阻塞線程
(2)實(shí)現(xiàn)原理:每次countDownLatch都執(zhí)行release(1)減1,當(dāng)減到0時(shí)調(diào)用unpark喚醒阻塞線程;即:state != 0,線程阻塞;state = 0,線程繼續(xù)執(zhí)行;
(3)使用場(chǎng)景圖:


countDown.png

4.3 CyclicBarrier 循環(huán)屏障

4.3.1 使多個(gè)線程在cyclicBarrier上阻塞,直到滿足某條件放開(kāi)
cyclicbarrier.png
4.3.2 使用實(shí)例:
CyclicBarrier cyc = new CyclicBarrier(3);
for (int i = 0; i < 5; i++) {
  new Thread(() -> {
    try {
      System.out.println(Thread.current().getName());
      cyc.await();  // 未達(dá)到三個(gè)線程,則進(jìn)行線程阻塞;總計(jì)達(dá)到三個(gè)線程時(shí),繼續(xù)向下執(zhí)行
     System.out.println(Thread.current().getName());
     Thread.sleep(5000);
     System.out.println(Thread.current().getName());
    } catch (...) { }
  }
} 
4.3.3 使用場(chǎng)景:

實(shí)現(xiàn)人滿發(fā)車(chē)場(chǎng)景,線程池用于線程復(fù)用

AutoicInteger con = new AutoicInteger();
ThreadPoolExecutor thread = new ThreadPoolExecutor(5, 5, 1000, Timeunit.Seconds, new ArrayBlockingQueue<>(100), (r)->new Thread(r, con.addAndGet(1)), new ThreadPoolExecutor.Abortpolicy());
CyclicBarrier cyc = new CyclicBarrier(5, () -> System.out.print("start"));
for (int i = 0; i < 10; i++) {
  ThreadPoolExecutor.submit(new Runner(cyc));
}
4.3.4 實(shí)現(xiàn)流程:

(1)首先加獨(dú)占鎖, 鎖住int state
(2)進(jìn)入條件隊(duì)列,阻塞線程,并釋放鎖;由于finally塊中的釋放鎖lock.unlock();需重新獲取鎖
(3)由于僅有同步隊(duì)列邏輯實(shí)現(xiàn)中有喚醒線程,并重新獲取鎖的實(shí)現(xiàn),這里進(jìn)行對(duì)進(jìn)入同步隊(duì)列的復(fù)用
(4)喚醒同步隊(duì)列綁定的線程節(jié)點(diǎn)

4.3.5 兩種重要的隊(duì)列:

(1)同步等待隊(duì)列:主要用于維護(hù)獲取鎖失敗時(shí)入隊(duì)的線程
(2)條件等待隊(duì)列:調(diào)用await()方法時(shí)會(huì)釋放鎖,線程會(huì)加入條件隊(duì)列;
調(diào)用signal()喚醒時(shí)將條件隊(duì)列中線程節(jié)點(diǎn)移動(dòng)到同步隊(duì)列中,等待再次獲取鎖

4.4 ReentrantReadWriteLock

針對(duì)讀多寫(xiě)少場(chǎng)景,該工具特性為:讀讀可并發(fā);寫(xiě)寫(xiě)、寫(xiě)讀互斥;提高讀寫(xiě)場(chǎng)景并發(fā)量

4.4.1 總結(jié),實(shí)現(xiàn)一把鎖有哪些設(shè)計(jì)?

(1)cas + 自旋嘗試
(2)管程方式:synchronized
(3)AQS方式:Cas + 同步隊(duì)列
(4)實(shí)現(xiàn)tryAcquired()方法 -> ReentrantLock等

4.4.2 如何設(shè)計(jì)并實(shí)現(xiàn)一把讀寫(xiě)分離鎖?

答:采用高地位打標(biāo)設(shè)計(jì)
高16位不為0:有讀鎖,每多一位表示多一個(gè)線程持有讀鎖,最高為65535個(gè)
低16位不為0:有寫(xiě)鎖,沒(méi)多一位表示多一次重入

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容