自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

阿里社招二面:談談你對JUC 中 AQS的理解,用了什么設計模式?為什么它是鎖的靈魂?

開發(fā) 前端
在 Java 中,鎖大多都是通過管程來實現(xiàn)的,比如大家熟悉的 Synchronized、AQS。這里先通過信號量、管程的對比幫助大家開始了解 AQS 的設計。

信號量和管程

在并發(fā)編程領域有幾個核心概念:

  • 互斥:只有一個線程能訪問臨界區(qū)。
  • 臨界資源:多個線程可以共享系統(tǒng)中的資源,但是臨界資源在同一時刻只允許一個線程訪問。
  • 臨界區(qū):訪問臨界資源的代碼即臨界區(qū)。

管程和信號量是操作系統(tǒng)中實現(xiàn)并發(fā)編程的兩種重要技術。

  • 信號量:是一種低級的同步工具,是一個計數(shù)器,用于控制對共享資源的訪問。信號量的值表示可用的資源數(shù)量。

主要包含共享變量 S、P 操作(申請資源)和 V 操作(釋放資源)。P 操作會使 S 值減一,當 S 值為負時,表示沒有資源可操作,此時要進入等待隊列;V 操作會使信號量的值加一,并喚醒等待隊列中的線程。

  • 管程:為了解決信號量在臨界區(qū)的 PV 操作上的配對的麻煩而提出的并發(fā)編程方法,使用條件變量等同步機制來實現(xiàn)線程之間的協(xié)作。

MESA 模型的 wait()是進入條件變量的等待隊列,當被 notify()或者 notifyAll()喚醒,會從條件變量等待隊列進入入口等待隊列。

小白:等等,什么是條件變量等待隊列呀?

打個比方,你去醫(yī)院看病,就診過程醫(yī)生先讓你去拍個 CT,于是你就去拍 CT 的隊列(條件隊列)排隊了,這時醫(yī)生可以給其他病人(線程)就診,那當你拍完 CT 拿到結果后(滿足條件變量)回來給醫(yī)生看,不是立馬執(zhí)行,而是需要先進入入口等待隊列里面,等待醫(yī)生給你看結果。

而這個場景下如果用信號量實現(xiàn),那會比較復雜,而且如果用不好,還會有死鎖的問題。

在 Java 中,鎖大多都是通過管程來實現(xiàn)的,比如大家熟悉的 Synchronized、AQS。這里先通過信號量、管程的對比幫助大家開始了解 AQS 的設計。

AQS 實現(xiàn)原理

Java 并發(fā)編程核心在于 java.cocurrent.util 包,而 juc 里面大多同步器的實現(xiàn)都有共同的特點:等待隊列、條件隊列、獨占獲取、共享獲取等,那么這個場景很容易就讓我們想到用模板方法的設計模式來實現(xiàn)。

在 AQS 中實現(xiàn)了鎖的獲取釋放框架,實際邏輯由子類去實現(xiàn),而核心的隊列入隊出隊操作在 AQS 父類抽象出來,正是基于這種抽象變與不變的思想,AQS 定義了一套多線程并發(fā)編程的抽象框架。

AQS 核心特性。

我們再來看下 AQS 的基本結構,它維護了一個共享資源 state 和一個 FIFO 的等待隊列,底層通過 CAS 機制保證了操作的原子性。

上文講過,AQS 是基于 MESA 模型實現(xiàn)的,所以在 AQS 中有兩種隊列:

  1. 同步等待隊列:AQS 的同步等待隊列也稱為 CLH 隊列,主要是 Craig、Landin、Hagersten 這三位大佬發(fā)明的一種基于雙向鏈表數(shù)據(jù)結構的隊列,是 FIFO 先入先出等待隊列。

  1. 條件等待隊列:Condition 是一個多線程間協(xié)調(diào)通信的工具,主要使某些線程一起等待某個條件,等具備該條件時,這些線程會被喚醒,從而進去等待隊列中爭奪鎖資源。

AQS 還定義了兩種資源獲取方式:

  1. Exclusive-獨占,只有一個線程能執(zhí)行成功,如 ReentrantLock。
  2. Share-共享,多個線程可以同時執(zhí)行成功,如 Semaphore/CountDownLatch,當然還有讀寫鎖的讀鎖,因為不涉及數(shù)據(jù)一致性問題,也是通過共享模式獲取資源。

在 AQS 中,不同場景下,不同的同步器爭搶資源的方式不同,但是不同的同步器只需要共享資源 state 的獲取和釋放方法即可,至于線程等待隊列的維護(比如入隊/喚醒出隊)在 AQS 頂層已實現(xiàn)好,如果你要自定義一個同步器,通常需要實現(xiàn)以下幾個方法:

  • isHeldExclusively:該線程是否正在獨占資源
  • tryAcquire(int):獨占方法。嘗試獲取資源,成功返回 true,失敗返回 false。
  • tryRelease(int):獨占方法。嘗試釋放資源,成功返回 true,失敗返回 false。
  • tryAcquireShared(int):獨占方法。嘗試獲取資源,負數(shù)表示失敗,大于等于 0 表示成功。
  • tryReleaseShared(int):獨占方法。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待節(jié)點返回 true,否則返回 false。

那么,你知道 JUC 中不同鎖/同步器都是怎么實現(xiàn)的嗎?

AQS 源碼分析

ReentrantLock 是我們經(jīng)常使用到一種鎖,下面我們以它為例子,分析它是如何實現(xiàn)獲取和釋放鎖資源的,一起來揭開 AQS 的神秘面紗。

我們都知道 ReentrantLock 是獨占鎖,有公平和非公平鎖兩種模式。

什么是公平和非公平鎖?

  • 公平鎖:指多個線程按照申請鎖的順序來獲取鎖,即按照線程的先后順序排隊獲取鎖。當一個線程釋放鎖后,等待時間最長的線程會獲得鎖的訪問權,保證每個線程都有機會獲取到鎖,避免饑餓現(xiàn)象的發(fā)生。
  • 非公平鎖:指多個線程獲取鎖的順序是不確定的,不按照申請鎖的順序排隊。一個線程在等待鎖時,有可能在其他線程釋放鎖后立即獲取鎖,允許某些線程相對于其他線程具有更高的獲取鎖的機會。

我們先來看下 ReentrantLock 相關核心類的關系。

FairSync 和 NoneFairSync 是 ReentrantLock 實現(xiàn)的內(nèi)部類,ReentrantLock 公平鎖和非公平鎖就是通過它們來實現(xiàn)的。

lock

然后再來看下 lock()方法的流程。

由上面可看出,ReentrantLock 實現(xiàn)的公平鎖、非公平鎖唯一的區(qū)別在于,非公平鎖在一開始調(diào)用獲取資源方式時,就直接嘗試獲取鎖,不會判斷等待隊列是否有線程在等待,獲取不到時,再把線程添加到等待隊列中。

小白:我有個問題,把線程節(jié)點添加到隊列尾部后,為啥還要調(diào)用 acquireQueued 方法判斷是否要掛起呀?

這個問題提得好,我們先來思考下,假設在線程獲取鎖資源失敗把線程節(jié)點添加到隊列中直接就掛起阻塞,意味著線程運行狀態(tài)轉(zhuǎn)換為阻塞,會帶來 CPU 從用戶態(tài)與內(nèi)核態(tài)之間轉(zhuǎn)換的兩次操作(阻塞和喚醒),特別在并發(fā)場景下,這種切換會帶來較大的性能開銷,所以 AQS 在入隊時首先會讓線程通過自旋的方式來等待競爭鎖。

小白:那么這里 acquireQueued 方法是如何實現(xiàn)的呢?

先看下核心源碼。

final boolean acquireQueued(final Node node, int arg) {
        // 獲取鎖資源標識
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
                // 獲取當前節(jié)點的前驅(qū)節(jié)點
                final Node p = node.predecessor();
                // 當前節(jié)點的前驅(qū)節(jié)點為頭節(jié)點,并獲取鎖資源成功
                if (p == head && tryAcquire(arg)) {
                    //把當前節(jié)點設置為頭節(jié)點
                    setHead(node);
                    // 原頭節(jié)點的下節(jié)點指向設置為null,方便GC回收
                    p.next = null; // help GC
                    // 設置鎖資源獲取成功
                    failed = false;
                    return interrupted;
                }
                // 如果當前節(jié)點不是head的下一節(jié)點/獲取鎖資源失敗,嘗試掛起線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

通過源碼,我們發(fā)現(xiàn)它主要是根據(jù)上一節(jié)點的狀態(tài)來判斷是否需要掛起,那么我們先看下 Node 有哪幾個狀態(tài)。

  • CANCELLED:1,線程已被取消。
  • SIGNAL:-1,等待隊列中存在待被喚醒的掛起線程。
  • CONDITION:-2,當前線程在 Condition 隊列中,未在 AQS 隊列中。
  • PROPAGATE:-3,表示后續(xù)結點會傳播喚醒的操作,共享模式下起作用。

通過流程圖分析。

以上就是獲取鎖的全部流程啦,怎么樣,通過流程圖分析后是不是覺得很簡單呢。

小白:嗯嗯,我還有一個疑問,為什么 acquireQueued 方法里面還要判斷線程是否中斷呢?

嗯不錯,你看得很細,一般線程中斷可以按中斷時線程狀態(tài)分為兩種:1、運行時中斷;2、阻塞或等待線程中斷。一般有中斷時,運行時的線程會在某個取消點中斷執(zhí)行,其實這也可以理解,因為如果立刻中斷,那么容易造成對象狀態(tài)不一致的情況發(fā)生。而阻塞或等待狀態(tài)的線程大多會立即響應中斷。

但是 JUC 中獲取獨占鎖的阻塞狀態(tài)不會立即響應中斷,這里在 acquireQueued 方法中對線程的中斷狀態(tài)判斷,如果中斷了返回 true,執(zhí)行 selfInterrupt 方法進入中斷狀態(tài),但注意是在獲取鎖之后,在獲取到鎖之前是不會做出響應的。

unLock

看完了 lock 方法,我們再來看下 unlock 釋放資源的實現(xiàn),ReentrantLock 實際調(diào)用的是 AQS 的 release 方法。

核心代碼:

public final boolean release(int arg) {
        //嘗試釋放鎖,返回鎖資源的計數(shù)值
        if (tryRelease(arg)) {
            //獲取等待隊列頭節(jié)點
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //喚醒等待隊列中待喚醒的節(jié)點
                unparkSuccessor(h);
            //表示完全釋放鎖資源
            return true;
        }
        //表示未完全釋放鎖資源
        return false;
    }

進去 release 方法,發(fā)現(xiàn)實際調(diào)用的還是 ReentrantLock 自己實現(xiàn)的 tryRelease 方法。

protected final boolean tryRelease(int releases) {
        //修改AQS的state
        int c = getState() - releases;
        //當前線程不是持有鎖線程,拋出異常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        //是否完全釋放鎖資源標識
        boolean free = false;
        if (c == 0) {
            //修改標識,表示完全釋放
            free = true;
            //將占用鎖資源的屬性設置為null
            setExclusiveOwnerThread(null);
        }
        //設置state值
        setState(c);
        //為true表示當前線程完全釋放資源
        //為false表示當前線程未完全釋放
        return free;
    }

以上就是釋放資源的實現(xiàn)原理。

好了,通過對 ReentrantLock 的實現(xiàn)分析完后,你對 AQS 底層的原理是不是了解得更多了呢?那么你知道怎么學習其他同步器都是如何實現(xiàn)的了嗎?

最后,我們再看來看一個問題,為什么 AQS 要使用雙向鏈表呢?

首先,我們來看下雙向鏈表的特點,雙向鏈表有兩個指針,一個指針指向前置節(jié)點,一個指針指向后繼節(jié)點,因此可以快速找到前置節(jié)點。雙向鏈表支持在兩端進行高效的操作,尾部添加新節(jié)點,頭部移除節(jié)點??梢员WC先進先出的順序,實現(xiàn)一定的公平性。

AQS 在多個地方需要獲取前置節(jié)點的信息,比如在入隊時需要判斷前置節(jié)點的狀態(tài)來決定是否阻塞;

在線程自旋阻塞時,只有頭節(jié)點的下一節(jié)點才需要競爭鎖,否則全部都去爭搶會造成羊群效應,為了避免這個問題,加入到鏈表的節(jié)點在爭搶鎖之前需要判斷前置節(jié)點是否頭節(jié)點。

而在單向鏈表中,去查找前置節(jié)點的效率顯然比雙向鏈表低很多。

擴展:CountDownLatch 是如何實現(xiàn)的呢?

責任編輯:姜華 來源: 碼哥跳動
相關推薦

2025-03-07 00:11:00

JWTJSONSession

2022-06-30 09:10:33

NoSQLHBaseRedis

2023-11-28 12:25:02

多線程安全

2021-10-27 20:54:24

分庫分表高并發(fā)

2022-10-18 08:38:16

內(nèi)存泄漏線程

2024-09-29 09:50:05

2021-04-25 09:58:48

mmapJava面試

2021-03-17 15:54:32

IO零拷貝方式

2022-09-06 11:13:16

接口PipelineHandler

2024-03-22 13:31:00

線程策略線程池

2025-01-13 09:24:32

2022-09-19 07:57:59

云服務互聯(lián)網(wǎng)基礎設施

2024-11-01 16:18:52

2024-09-20 05:46:00

2024-09-11 16:49:55

2014-12-19 09:59:50

代碼

2022-09-28 16:37:59

SpringMVC框架

2024-03-15 09:06:48

HTTPSCA私鑰

2024-02-04 15:53:20

C++函數(shù)編程語言

2015-07-28 14:22:09

BAT
點贊
收藏

51CTO技術棧公眾號