Java 併發基礎常見面試題總結

原文鏈接:  https://github.com/Snailclimb/JavaGuide#java

版權原作者所有,此處個人記錄備用

1. 什麼是線程和進程?

1.1. 何爲進程?

進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。

在 Java 中,當我們啓動 main 函數時其實就是啓動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。

在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。

1.2. 何爲線程?

線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器虛擬機棧本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因爲如此,線程也被稱爲輕量級進程。

Java 程序天生就是多線程程序,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下。

public class MultiThread {
	public static void main(String[] args) {
		// 獲取 Java 線程管理 MXBean
	ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
		// 不需要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息
		ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
		// 遍歷線程信息,僅打印線程 ID 和線程名稱信息
		for (ThreadInfo threadInfo : threadInfos) {
			System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
		}
	}
}

上述程序輸出如下(輸出內容可能不同,不用太糾結下面每個線程的作用,只用知道 main 線程執行 main 方法即可):

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分發處理給 JVM 信號的線程
[3] Finalizer //調用對象 finalize 方法的線程
[2] Reference Handler //清除 reference 線程
[1] main //main 線程,程序入口

從上面的輸出內容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行

2. 請簡要描述線程與進程的關係,區別及優缺點?

從 JVM 角度說進程和線程之間的關係

2.1. 圖解進程和線程的關係

下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關係。如果你對 Java 內存區域 (運行時數據區) 這部分知識不太瞭解的話可以閱讀一下這篇文章:《可能是把 Java 內存區域講的最清楚的一篇文章》

 

從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器虛擬機棧 和 本地方法棧

總結: 線程 是 進程 劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因爲同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反

下面來思考這樣一個問題:爲什麼程序計數器虛擬機棧本地方法棧是線程私有的呢?爲什麼堆和方法區是線程共享的呢?

2.2. 程序計數器爲什麼是私有的?

程序計數器主要有下面兩個作用:

  1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

需要注意的是,如果執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的纔是下一條指令的地址。

所以,程序計數器私有主要是爲了線程切換後能恢復到正確的執行位置

2.3. 虛擬機棧和本地方法棧爲什麼是私有的?

  • 虛擬機棧: 每個 Java 方法在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
  • 本地方法棧: 和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。

所以,爲了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

2.4. 一句話簡單瞭解堆和方法區

堆和方法區是所有線程共享的資源,其中是進程中最大的一塊內存,主要用於存放新創建的對象 (所有對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

3. 說說併發與並行的區別?

  • 併發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);
  • 並行: 單位時間內,多個任務同時執行。

4. 爲什麼要使用多線程呢?

先從總體上來說:

  • 從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷
  • 從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能

再深入到計算機底層來探討:

  • 單核時代: 在單核時代多線程主要是爲了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閒;進行 IO 操作時,CPU 空閒。我們可以簡單地說這兩者的利用率目前都是 50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO 操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
  • 多核時代: 多核時代多線程主要是爲了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

5. 使用多線程可能帶來什麼問題?

併發編程的目的就是爲了能提高程序的執行效率提高程序運行速度,但是併發編程並不總是能提高程序運行速度的,而且併發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閒置問題

 

6. 說說線程的生命週期和狀態?

Java 線程在運行的生命週期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。

 

線程在生命週期中並不是固定處於某一個狀態而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 併發編程藝術》4.1.4 節):

Java线ç¨ç¶æåè¿

由上圖可以看出:線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法後開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)後就處於 RUNNING(運行) 狀態。

 

操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源:HowToDoInJavaJava Thread Life Cycle and Thread States),所以 Java 系統一般將這兩個狀態統稱爲 RUNNABLE(運行中) 狀態 。

 

RUNNABLE-VS-RUNNING

當線程執行 wait()方法之後,線程進入 WAITING(等待) 狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達後 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。

7. 什麼是上下文切換?

多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能得到有效執行,CPU 採取的策略是爲每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。

概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換會這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

8. 什麼是線程死鎖?如何避免死鎖?

8.1. 認識線程死鎖

多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態。

线ç¨æ­»é示æå¾

 

下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源於《併發編程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}

Output

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

線程 A 通過 synchronized (resource1) 獲得 resource1 的監視器鎖,然後通過 Thread.sleep(1000);讓線程 A 休眠 1s 爲的是讓線程 B 得到執行然後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

學過操作系統的朋友都知道產生死鎖必須具備以下四個條件:

  1. 互斥條件:該資源任意一個時刻只由一個線程佔用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪,只有自己使用完畢後才釋放資源。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

8.2. 如何避免線程死鎖?

我們只要破壞產生死鎖的四個條件中的其中一個就可以了。

破壞互斥條件

這個條件我們沒有辦法破壞,因爲我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

破壞請求與保持條件

一次性申請所有的資源。

破壞不剝奪條件

佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源。

破壞循環等待條件

靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

我們對線程 2 的代碼修改成下面這樣就不會產生死鎖了。

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 2").start();

Output

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2

Process finished with exit code 0

我們分析一下上面的代碼爲什麼避免了死鎖的發生?

線程 1 首先獲得到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。然後線程 1 再去獲取 resource2 的監視器鎖,可以獲取到。然後線程 1 釋放了對 resource1、resource2 的監視器鎖的佔用,線程 2 獲取到就可以執行了。這樣就破壞了破壞循環等待條件,因此避免了死鎖。

9. 說說 sleep() 方法和 wait() 方法區別和共同點?

  • 兩者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
  • 兩者都可以暫停線程的執行。
  • Wait 通常被用於線程間交互/通信,sleep 通常被用於暫停執行。
  • wait() 方法被調用後,線程不會自動甦醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成後,線程會自動甦醒。或者可以使用wait(long timeout)超時後線程會自動甦醒。

10. 爲什麼我們調用 start() 方法時會執行 run() 方法,爲什麼我們不能直接調用 run() 方法?

這是另一個非常經典的 java 多線程面試問題

new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就可以開始運行了。 start() 會執行線程的相應準備工作,然後自動執行 run() 方法的內容,這是真正的多線程工作。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,所以這並不是多線程工作。

總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,還是在主線程裏執行。

11. synchronized 關鍵字

11.1. 說一說自己對於 synchronized 關鍵字的瞭解

synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。

另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的 synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷

11.2. 說說自己是怎麼使用 synchronized 關鍵字,在項目中用到了嗎

synchronized關鍵字最主要的三種使用方式:

  • 修飾實例方法: 作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
  • 修飾靜態方法: :也就是給當前類加鎖,會作用於類的所有對象實例,因爲靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份)。所以如果一個線程A調用一個實例對象的非靜態 synchronized 方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖
  • 修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。儘量不要使用 synchronized(String a) 因爲JVM中,字符串常量池具有緩存功能!

下面我以一個常見的面試題爲例講解一下 synchronized 關鍵字的具體使用。

面試中面試官經常會說:“單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單例模式的原理唄!”

雙重校驗鎖實現對象單例(線程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼
        if (uniqueInstance == null) {
            //類對象加鎖
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

另外,需要注意 uniqueInstance 採用 volatile 關鍵字修飾也是很有必要。

uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分爲三步執行:

  1. 爲 uniqueInstance 分配內存空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的內存地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行

11.3. 講一下 synchronized 關鍵字的底層原理

synchronized 關鍵字底層原理屬於 JVM 層面。

① synchronized 同步語句塊的情況

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代碼塊");
		}
	}
}

通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯後的 .class 文件,然後執行javap -c -s -v -l SynchronizedDemo.class

synchronizedå³é®å­åç

從上面我們可以看出:

synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在於每個Java對象的對象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是爲什麼Java中任意對象可以作爲鎖的原因) 的持有權。當計數器爲0則可以成功獲取,獲取後將鎖計數器設爲1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設爲0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放爲止。

② synchronized 修飾方法的的情況

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

synchronized關鍵字原理

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。

11.4. 說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎

JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。

鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是爲了提高獲得鎖和釋放鎖的效率。

關於這幾種優化的詳細信息可以查看筆主的這篇文章:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md

11.5. 談談 synchronized和ReentrantLock 的區別

① 兩者都是可重入鎖

兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降爲0時才能釋放鎖。

② synchronized 依賴於 JVM 而 ReentrantLock 依賴於 API

synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。ReentrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。

③ ReentrantLock 比 synchronized 增加了一些高級功能

相比synchronized,ReentrantLock增加了一些高級功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以綁定多個條件)

  • ReentrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改爲處理其他事情。
  • ReentrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReentrantLock默認情況是非公平的,可以通過 ReentrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
  • synchronized關鍵字與wait()和notify()/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition接口與newCondition() 方法。Condition是JDK1.5之後纔有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),線程對象可以註冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用notify()/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現“選擇性通知” ,這個功能非常重要,而且是Condition接口默認提供的。而synchronized關鍵字就相當於整個Lock對象中只有一個Condition實例,所有的線程都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒註冊在該Condition實例中的所有等待線程。

如果你想使用上述功能,那麼選擇ReentrantLock是一個不錯的選擇。

④ 性能已不是選擇標準

12. volatile關鍵字

12.1. 講一下Java內存模型

在 JDK1.2 之前,Java的內存模型實現總是從主存(即共享內存)讀取變量,是不需要進行特別的注意的。而在當前的 Java 內存模型下,線程可以把變量保存本地內存比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致

要解決這個問題,就需要把變量聲明爲volatile,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。

 

說白了, volatile 關鍵字的主要作用就是保證變量的可見性然後還有一個作用是防止指令重排序

 

12.2. 說說 synchronized 關鍵字和 volatile 關鍵字的區別

synchronized關鍵字和volatile關鍵字比較

  • volatile關鍵字是線程同步的輕量級實現,所以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字只能用於變量而synchronized關鍵字可以修飾方法以及代碼塊。synchronized關鍵字在JavaSE1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些
  • 多線程訪問volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞
  • volatile關鍵字能保證數據的可見性,但不能保證數據的原子性。synchronized關鍵字兩者都能保證。
  • volatile關鍵字主要用於解決變量在多個線程之間的可見性,而 synchronized關鍵字解決的是多個線程之間訪問資源的同步性。

13. ThreadLocal

13.1. ThreadLocal簡介

通常情況下,我們創建的變量是可以被任何一個線程訪問並修改的。如果想實現每一個線程都有自己的專屬本地變量該如何解決呢? JDK中提供的ThreadLocal類正是爲了解決這樣的問題。 ThreadLocal類主要解決的就是讓每個線程綁定自己的值,可以將ThreadLocal類形象的比喻成存放數據的盒子,盒子中可以存儲每個線程的私有數據。

如果你創建了一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的本地副本,這也是ThreadLocal變量名的由來。他們可以使用 get() 和 set() 方法來獲取默認值或將其值更改爲當前線程所存的副本的值,從而避免了線程安全問題。

再舉個簡單的例子:

比如有兩個人去寶屋收集寶物,這兩個共用一個袋子的話肯定會產生爭執,但是給他們兩個人每個人分配一個袋子的話就不會出現這樣的問題。如果把這兩個人比作線程的話,那麼ThreadLocal就是用來避免這兩個線程競爭的。

3.2. ThreadLocal示例

相信看了上面的解釋,大家已經搞懂 ThreadLocal 類是個什麼東西了。

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是線程安全的,所以每個線程都要有自己獨立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}

Output:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

從輸出中可以看出,Thread-0已經改變了formatter的值,但仍然是thread-2默認格式化程序與初始化值相同,其他線程也一樣。

上面有一段代碼用到了創建 ThreadLocal 變量的那段代碼用到了 Java8 的知識,它等於下面這段代碼,如果你寫了下面這段代碼的話,IDEA會提示你轉換爲Java8的格式(IDEA真的不錯!)。因爲ThreadLocal類在Java 8中擴展,使用一個新的方法withInitial(),將Supplier功能接口作爲參數。

 private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

3.3. ThreadLocal原理

從 Thread類源代碼入手。

public class Thread implements Runnable {
 ......
//與此線程有關的ThreadLocal值。由ThreadLocal類維護
ThreadLocal.ThreadLocalMap threadLocals = null;

//與此線程有關的InheritableThreadLocal值。由InheritableThreadLocal類維護
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

從上面Thread類 源代碼可以看出Thread 類中有一個 threadLocals 和 一個 inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類型的變量,我們可以把 ThreadLocalMap 理解爲ThreadLocal 類實現的定製化的 HashMap。默認情況下這兩個變量都是null,只有當前線程調用 ThreadLocal 類的 setget方法時才創建它們,實際上調用這兩個方法的時候,我們調用的是ThreadLocalMap類對應的 get()set() 方法。

ThreadLocal類的set()方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

通過上面這些內容,我們足以通過猜測得出結論:最終的變量是放在了當前線程的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解爲只是ThreadLocalMap的封裝,傳遞了變量值。 ThrealLocal 類中可以通過Thread.currentThread()獲取到當前線程對象後,直接通過getMap(Thread t)可以訪問到該線程的ThreadLocalMap對象。

每個Thread中都具備一個ThreadLocalMap,而ThreadLocalMap可以存儲以ThreadLocal爲key的鍵值對。 比如我們在同一個線程中聲明瞭兩個 ThreadLocal 對象的話,會使用 Thread內部都是使用僅有那個ThreadLocalMap 存放數據的,ThreadLocalMap的 key 就是 ThreadLocal對象,value 就是 ThreadLocal 對象調用set方法設置的值。ThreadLocal 是 map結構是爲了讓每個線程可以關聯多個 ThreadLocal變量。這也就解釋了 ThreadLocal 聲明的變量爲什麼在每一個線程都有自己的專屬本地變量。

ThreadLocalMapThreadLocal的靜態內部類。

 

3.4. ThreadLocal 內存泄露問題

ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,而 value 是強引用。所以,如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會 key 會被清理掉,而 value 不會被清理掉。這樣一來,ThreadLocalMap 中就會出現key爲null的Entry。假如我們不做任何措施的話,value 永遠無法被GC 回收,這個時候就可能會產生內存泄露。ThreadLocalMap實現中已經考慮了這種情況,在調用 set()get()remove() 方法的時候,會清理掉 key 爲 null 的記錄。使用完 ThreadLocal方法後 最好手動調用remove()方法

      static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

弱引用介紹:

如果一個對象只具有弱引用,那就類似於可有可無的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

 

14. 線程池

14.1. 爲什麼要用線程池?

線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計信息,例如已完成任務的數量。

這裏借用《Java併發編程的藝術》提到的來說一下使用線程池的好處:

  • 降低資源消耗。 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。 當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

14.2. 實現Runnable接口和Callable接口的區別

如果想讓線程池執行任務的話需要實現的Runnable接口或Callable接口。 Runnable接口或Callable接口實現類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。兩者的區別在於 Runnable 接口不會返回結果但是 Callable 接口可以返回結果。

備註: 工具類Executors可以實現Runnable對象和Callable對象之間的相互轉換。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

14.3. 執行execute()方法和submit()方法的區別是什麼呢?

1)execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功與否;

2)submit() 方法用於提交需要返回值的任務。線程池會返回一個Future類型的對象,通過這個Future對象可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

14.4. 如何創建線程池

《阿里巴巴Java開發手冊》中強制線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險

Executors 返回線程池對象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度爲 Integer.MAX_VALUE ,可能堆積大量的請求,從而導致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允許創建的線程數量爲 Integer.MAX_VALUE ,可能會創建大量線程,從而導致OOM。

方式一:通過構造方法實現 

ThreadPoolExecutoræé æ¹æ³

方式二:通過Executor 框架的工具類Executors來實現 我們可以創建三種類型的ThreadPoolExecutor:

  • FixedThreadPool : 該方法返回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務提交時,線程池中若有空閒線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閒時,便處理在任務隊列中的任務。
  • SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多餘一個任務被提交到該線程池,任務會被保存在一個任務隊列中,待線程空閒,按先入先出的順序執行隊列中的任務。
  • CachedThreadPool: 該方法返回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不確定,但若有空閒線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務提交,則會創建新的線程處理任務。所有線程在當前任務執行完畢後,將返回線程池進行復用。

對應Executors工具類中的方法如圖所示: 

Executoræ¡æ¶çå·¥å·ç±»

15. Atomic 原子類

15.1. 介紹一下Atomic 原子類

Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裏 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

所以,所謂原子類說簡單點就是具有原子/原子操作特徵的類

併發包 java.util.concurrent 的原子類都存放在java.util.concurrent.atomic下,如下圖所示。

JUC原子類概覽

15.2. JUC 包中的原子類是哪4類?

基本類型

使用原子的方式更新基本類型

  • AtomicInteger:整形原子類
  • AtomicLong:長整型原子類
  • AtomicBoolean:布爾型原子類

數組類型

使用原子的方式更新數組裏的某個元素

  • AtomicIntegerArray:整形數組原子類
  • AtomicLongArray:長整形數組原子類
  • AtomicReferenceArray:引用類型數組原子類

引用類型

  • AtomicReference:引用類型原子類
  • AtomicStampedReference:原子更新引用類型裏的字段原子類
  • AtomicMarkableReference :原子更新帶有標記位的引用類型

對象的屬性修改類型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新長整形字段的更新器
  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於解決原子的更新數據和數據的版本號,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。

15.3. 講講 AtomicInteger 的使用

AtomicInteger 類常用方法

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置之後可能導致其他線程在之後的一小段時間內還是可以讀到舊的值

AtomicInteger 類的使用示例

使用 AtomicInteger 之後,不用對 increment() 方法加鎖也可以保證線程安全。

class AtomicIntegerTest {
        private AtomicInteger count = new AtomicInteger();
      //使用AtomicInteger之後,不需要對該方法加鎖,也可以實現線程安全。
        public void increment() {
                  count.incrementAndGet();
        }
     
       public int getCount() {
                return count.get();
        }
}

15.4. 能不能給我簡單介紹一下 AtomicInteger 類的原理

AtomicInteger 線程安全原理簡單分析

AtomicInteger 類的部分源碼:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較並替換”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大爲提升。

CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。

16. AQS

16.1. AQS 介紹

AQS的全稱爲(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。

AQSç±»

 

AQS是一個用來構建鎖同步器框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。當然,我們自己也能利用AQS非常輕鬆容易地構造出符合我們自己需求的同步器。

16.2. AQS 原理分析

AQS 原理這部分參考了部分博客,併發編程面試必備:AQS 原理以及 AQS 同步組件總結

在面試中被問到併發知識的時候,大多都會被問到“請你說一下自己對於AQS原理的理解”。下面給大家一個示例供大家參加,面試不是背題,大家一定要加入自己的思想,即使加入不了自己的思想也要保證自己能夠通俗的講出來而不是背出來。

下面大部分內容其實在AQS類註釋上已經給出了,不過是英語看着比較喫力一點,感興趣的話可以看看源碼。

16.2.1. AQS 原理概覽

AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中

CLH(Craig,Landin,and Hagersten)隊列是一個虛擬雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。

看個AQS(AbstractQueuedSynchronizer)原理圖:

AQSåçå¾

 

AQS使用一個int成員變量來表示同步狀態,通過內置的FIFO隊列來完成獲取資源線程的排隊工作。AQS使用CAS對該同步狀態進行原子操作實現對其值的修改。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性

狀態信息通過protected類型的getState,setState,compareAndSetState進行操作

//返回同步狀態的當前值
protected final int getState() {  
        return state;
}
 // 設置同步狀態的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)將同步狀態值設置爲給定值update如果當前同步狀態的值等於expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

16.2.2. AQS 對資源的共享方式

AQS定義兩種資源共享方式

  • Exclusive(獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:
    • 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
    • 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
  • Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。

ReentrantReadWriteLock 可以看成是組合式,因爲ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。

16.2.3. AQS底層使用了模板方法模式

同步器的設計是基於模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經典的一個應用):

  1. 使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源state的獲取和釋放)
  2. 將AQS組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。

這和我們以往通過實現接口的方式有很大區別,這是模板方法模式很經典的一個運用。

AQS使用了模板方法模式,自定義同步器時需要重寫下面幾個AQS提供的模板方法:

isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才需要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。

默認情況下,每個方法都拋出 UnsupportedOperationException。 這些方法的實現必須是內部線程安全的,並且通常應該簡短而不是阻塞。AQS類中的其他方法都是final ,所以無法被其他類使用,只有這幾個方法可以被其他類使用。

以ReentrantLock爲例,state初始化爲0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨佔該鎖並將state+1。此後,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。

再以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每個子線程執行完後countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數返回,繼續後餘動作。

一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock

推薦兩篇 AQS 原理和相關源碼分析的文章:

16.3. AQS 組件總結

  • Semaphore(信號量)-允許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。
  • CountDownLatch (倒計時器): CountDownLatch是一個同步工具類,用來協調多個線程之間的同步。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。
  • CyclicBarrier(循環柵欄): CyclicBarrier 和 CountDownLatch 非常類似,它也可以實現線程間的技術等待,但是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 類似。CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。CyclicBarrier默認的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await()方法告訴 CyclicBarrier 我已經到達了屏障,然後當前線程被阻塞。

17 Reference

 

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