《Java并發(fā)編程之美》學(xué)習(xí)筆記

1. 并發(fā)編程基礎(chǔ)

1.1 什么是線程

線程是進(jìn)程中的一個實體,線程本身是不會獨立存在的。進(jìn)程是代碼在數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位,線程則是進(jìn)程的一個執(zhí)行路徑,一個進(jìn)程中至少有一個線程,進(jìn)程中的多個線程共享進(jìn)程的資源

操作系統(tǒng)在分配資源時是把資源分配給進(jìn)程的,但是 CPU 資源比較特殊,它是被分配到線程的,因為要真正占用 CPU 運行的是線程,所以也說線程是 CPU 分配的基本單位

多個線程共享進(jìn)程的堆和方法區(qū)資源,但每個線程有自己的程序計數(shù)器和棧區(qū)域

  • 程序計數(shù)器是一塊內(nèi)存區(qū)域,用來記錄線程當(dāng)前要執(zhí)行的指令地址
  • 需要注意的是,如果執(zhí)行的是 native 方法,那么 pc 計數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時 pc 計數(shù)器記錄的才是下一條指令的地址
  • 每個線程都有自己的棧資源,用于存儲該線程的局部變量,這些局部變量是該線程私有的,其他線程是訪問不了的,除此之外棧還用來存放線程的調(diào)用棧幀
  • 堆是一個進(jìn)程中最大的一塊內(nèi)存,堆是被進(jìn)程中的所有線程共享的,是進(jìn)程創(chuàng)建時分配的,堆里面主要存放使用 new 操作創(chuàng)建的對象實例
  • 方法區(qū)則用來存放 JVM 加載的類、常量及靜態(tài)變量等信息,也是線程共享的

1.2 線程三種創(chuàng)建方式的優(yōu)缺點

Java 中有三種線程創(chuàng)建方式,分別為實現(xiàn) Runnable 接口的 run 方法,繼承 Thread 類并重寫 run 方法,以及使用 FutureTask 方式

使用繼承方式的好處是方便傳參,可以在子類里添加成員變量,通過 set 方法設(shè)置參數(shù)或者通過構(gòu)造函數(shù)進(jìn)行傳遞,而如果使用 Runnable 方式,則只能使用主線程里面被聲明為 final 的變量。不好的地方是 Java 不支持多繼承,而如果繼承了 Thread 類,那么子類不能再繼承其他類,而 Runnable 則沒有這個限制。前兩種方式都沒辦法拿到任務(wù)執(zhí)行的返回結(jié)果,但是 FutureTask 方式可以。

1.3 線程通知與等待

Java 中的 Object 類是所有類的父類,鑒于繼承機(jī)制,Java 把所有類都需要的方法放到了 Object 類里面,其中就包含通知與等待系列的函數(shù)

wait() / wait(long timeout)
當(dāng)一個線程調(diào)用一個共享變量的 wait() 方法時,該調(diào)用線程會被阻塞掛起,直到發(fā)生下面幾件事情之一才返回:

1.其他線程調(diào)用了該共享對象的 notify() 或者 notifyAll() 方法
2.其他線程調(diào)用了該線程的 interrupt() 方法,該線程拋出 InterruptedException 異常返回
3.如果帶有超時參數(shù),沒有在指定時間的 timeout ms 時間內(nèi)被其他線程調(diào)用該共享變量的 notify() 或者 notifyAll() 方法喚醒,那么該函數(shù)還是會因為超時而返回
4.不加參數(shù)的 wait() 方法內(nèi)部就是調(diào)用了 wait(0)

當(dāng)線程調(diào)用共享對象的 wait() 方法時,當(dāng)前線程只會釋放當(dāng)前共享對象的鎖,當(dāng)前線程持有的其他共享對象的監(jiān)視器鎖并不會被釋放

虛假喚醒
一個線程可以從掛起狀態(tài)變?yōu)榭梢赃\行狀態(tài)(也就是被喚醒),即使該線程沒有被其他線程調(diào)用 notify()、notifyAll() 方法進(jìn)行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒 。

虛假喚醒在應(yīng)用實踐中很少發(fā)生,但要防患于未然,做法就是不停的測試該線程被喚醒的條件是否滿足,不滿足則繼續(xù)等待,也就是說在一個循環(huán)中調(diào)用 wait() 方法進(jìn)行防范。退出循環(huán)的條件是滿足了喚醒該線程的條件。

synchronized (obj) {
    while (條件不滿足) {
        obj.wait();
    }
}

notify()
一個線程調(diào)用共享對象的 notify() 方法后,會喚醒一個在該共享變量上調(diào)用 wait 系列方法后被掛起的線程。一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是隨機(jī)的。這個被喚醒的線程還需要和其他線程一起競爭該鎖,只有該線程競爭到了共享變量的監(jiān)視器鎖后才可以繼續(xù)執(zhí)行

notifyAll()
notifyAll() 方法會喚醒所有在該共享變量上由于調(diào)用 wait 系列方法而被掛起的線程

1.4 等待線程執(zhí)行終止的 join 方法

Thread 類中的 join 方法可以用來等待多個線程全部加載完畢再匯總處理

線程 A 調(diào)用線程 B 的 join 方法后會被阻塞,當(dāng)其他線程調(diào)用了線程 A 的 interrupt() 方法中斷了線程 A 時,線程 A 會拋出 InterruptedException 異常而返回

1.5 讓線程睡眠的 sleep 方法

Thread 類有一個靜態(tài)的 sleep() 方法,當(dāng)一個執(zhí)行中的線程調(diào)用了 Thread 的 sleep() 方法后,調(diào)用線程會暫時讓出指定的執(zhí)行權(quán),也就是在這期間不參與 CPU 的調(diào)度,但是該線程所擁有的監(jiān)視器資源,比如鎖還是持有不讓出的。指定的睡眠時間到了后該函數(shù)會正常返回,線程就處于就緒狀態(tài),然后參與 CPU 的調(diào)度,獲取到 CPU 的資源后就可以運行了。

1.6 讓出 CPU 執(zhí)行權(quán)的 yield 方法

當(dāng)一個線程調(diào)用了 Thread 類的靜態(tài)方法 yield() 時,是在告訴線程調(diào)度器自己占有的時間片中還沒有使用完的部分自己不想使用了,這暗示線程調(diào)度器現(xiàn)在就可以進(jìn)行下一輪的線程調(diào)度

sleep() 和 yield() 方法的區(qū)別在于,當(dāng)線程調(diào)用 sleep() 方法時調(diào)用線程會被阻塞掛起指定的時間,在這期間線程調(diào)度器不會去調(diào)度該線程。而調(diào)用 yield() 方法時,線程只是讓出自己剩余的時間片,并沒有被阻塞掛起,而是處于就緒狀態(tài),線程調(diào)度器下一次調(diào)度時就有可能調(diào)度到當(dāng)前線程執(zhí)行。

1.7 線程中斷

Java 中的線程中斷是一種線程間的協(xié)作模式,通過設(shè)置線程的中斷標(biāo)志并不能直接終止該線程的執(zhí)行,而是被中斷的線程根據(jù)中斷狀態(tài)自行處理

  • void interrupt() : 中斷線程,當(dāng)線程 A 運行時,線程 B 可以調(diào)用線程 A 的 interrupt() 方法來設(shè)置線程 A 的中斷標(biāo)志為 true 并立即返回。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志,線程 A 實際并沒有被中斷,它會繼續(xù)往下執(zhí)行。
  • boolean isInterrupted() : 檢測當(dāng)前線程是否被中斷,如果是返回 true,否則返回 false
  • boolean interrupted() : 檢測當(dāng)前線程是否被中斷,如果是返回 true,否則返回 false,如果該方法發(fā)現(xiàn)當(dāng)前線程被中斷,則會清除中斷標(biāo)志,并且該方法是 static 方法,可以通過 Thread 類直接調(diào)用。

1.8 線程上下文切換

線程上下文切換時機(jī)有:當(dāng)前線程的 CPU 時間片使用完處于就緒狀態(tài)時,當(dāng)前線程被其他線程中斷時

1.9 線程死鎖

什么是死鎖
死鎖是指兩個或兩個以上的線程在執(zhí)行過程,因爭奪資源而造成的互相等待的現(xiàn)象,在無外力作用的情況下,這些線程會一直等待而無法繼續(xù)運行下去
產(chǎn)生死鎖的條件。

死鎖的產(chǎn)生必須具備以下四個條件:

  • 互斥條件:指線程對已經(jīng)獲取到的資源進(jìn)行排它性使用,即該資源同時只由一個線程占用。如果此時還有其他線程請求使用該資源,則請求者只能等待,直至占有資源的線程釋放該資源
  • 請求并持有條件:指一個線程已經(jīng)持有了至少一個資源,但又提出了新的資源請求,而新資源已被其他線程占有,所以當(dāng)前線程會被阻塞,但阻塞的同時并不釋放自己已經(jīng)獲取的資源
  • 不可剝奪條件:指線程獲取到的資源在自己使用完之前不能被其他線程搶占,只有在自己使用完畢后才由自己釋放該資源
  • 環(huán)路等待條件:指在發(fā)生死鎖時,必然存在一個線程一資源的環(huán)形鏈,即線程集合 {T0, T1, T2, ... , Tn} 中的 T0 正在等待一個 T1 占用的資源,T1 正在等待 T2 占用的資源,......Tn 正在等待已被 T0 占用的資源。

如何避免線程死鎖
要想避免死鎖,只需要破壞掉至少一個構(gòu)造死鎖的必要條件即可,但是目前只有 請求并持有 和 環(huán)路等待 條件是可以被破壞的

資源的有序分配會避免死鎖,因為資源的有序性破壞了資源的請求并持有條件和環(huán)路等待條件,因此避免了死鎖。

1.10 守護(hù)線程與用戶線程

Java 中的線程分為兩類,分別為 daemon 線程(守護(hù)線程)和 user 線程(用戶線程)。在 JVM 啟動時會調(diào)用 main 函數(shù),main 函數(shù)所在的線程就是一個用戶線程,而垃圾回收線程則是守護(hù)線程

守護(hù)線程和用戶線程區(qū)別之一是當(dāng)最后一個非守護(hù)線程結(jié)束時,JVM 會正常退出,而不管當(dāng)前是否有守護(hù)線程,也就是說守護(hù)線程是否結(jié)束并不影響 JVM 的退出。言外之意,只要有一個用戶線程還沒結(jié)束,正常情況下 JVM 就不會退出

創(chuàng)建守護(hù)線程的的方式是,設(shè)置線程的 daemon 參數(shù)為 true 即可

總的來說,如果希望在主線程結(jié)束后 JVM 進(jìn)程馬上結(jié)束,那么在創(chuàng)建線程時可以將其設(shè)置為守護(hù)線程,如果希望在主線程結(jié)束后子線程繼續(xù)工作,等子線程結(jié)束后再讓 JVM 進(jìn)程結(jié)束,那么就將子線程設(shè)置為用戶線程

1.11 ThreadLocal

ThreadLocal 是 JDK 包提供的,它提供了線程本地變量,也就是如果你創(chuàng)建了一個 ThreadLocal 變量,那么訪問這個變量的每個線程都會有這個變量的一個本地副本。當(dāng)多個線程操作這個變量時,實際操作的是自己本地內(nèi)存里面的變量,從而避免了線程安全問題。

ThreadLocal 是一個 HashMap 結(jié)構(gòu),其中 key 就是當(dāng)前 ThreadLocal 的實例引用,value 是通過 set 方法傳遞的值。ThreadLocal 變量在父線程中被設(shè)置值后,在子線程中是獲取不到的。

2. 并發(fā)編程的其他基礎(chǔ)知識

2.1 為什么要進(jìn)行多線程并發(fā)編程

多核 CPU 時代的到來打破了單核 CPU 對多線程效能的限制。多個 CPU 意味著每個線程可以使用自己的 CPU 運行,這減少了線程上下文切換的開銷,但隨著對應(yīng)用系統(tǒng)性能和吞吐量要求的提高,出現(xiàn)了處理海量數(shù)據(jù)和請求的要求,這些都會高并發(fā)編程有著迫切的需求。

2.2 Java 中的線程安全問題

  • 共享資源:就是說該資源被多個線程所持有或者說多個線程都可以去訪問該資源

線程安全問題是指當(dāng)多個線程同時讀寫一個共享資源并且沒有任何同步措施時,導(dǎo)致出現(xiàn)臟數(shù)據(jù)或者其他不可預(yù)見的結(jié)果的問題

2.3 Java 中共享變量的內(nèi)存可見性問題

當(dāng)一個線程操作共享變量時,它首先從主內(nèi)存復(fù)制共享變量到自己的工作內(nèi)存,然后對工作內(nèi)存里的變量進(jìn)行處理,處理完后將變量值更新到主內(nèi)存

假如線程 A 和線程 B 使用不同的 CPU 執(zhí)行,此時由于 Cache 的存在,將會導(dǎo)致內(nèi)存不可見問題

2.4 synchronized

2.4.1 synchronized 關(guān)鍵字介紹

synchronized 塊是 Java 提供的一種原子性內(nèi)置鎖,Java 中的每個對象都可以把它當(dāng)做一個同步鎖來使用,這些 Java 內(nèi)置的使用者看不到的鎖被稱為 內(nèi)部鎖,也叫做 監(jiān)視器鎖 。

內(nèi)置鎖是排它鎖,也就是當(dāng)一個線程獲取這個鎖后,其他線程必須等待該線程釋放鎖后才能獲取該鎖。

另外,由于 Java 中的線程是與操作系統(tǒng)中的原生線程一一對應(yīng)的,所以當(dāng)阻塞一個線程時,需要從用戶態(tài)切換到內(nèi)核態(tài)執(zhí)行阻塞操作,這是很耗時的操作,而 synchronized 的使用就會導(dǎo)致上下文切換。

2.4.2 synchronized 的內(nèi)存語義

進(jìn)入 synchronized 塊的內(nèi)存語義是把在 synchronized 塊內(nèi)使用到的變量從線程的工作內(nèi)存中清除,這樣在 synchronized 塊內(nèi)使用到該變量時就不會從線程的工作內(nèi)存中獲取,而是直接從主內(nèi)存中獲取。退出 synchronized 塊的內(nèi)存語義是把在 synchronized 塊內(nèi)對共享變量的修改刷新到主內(nèi)存。

除了可以解決共享變量內(nèi)存可見性問題外,synchronized 經(jīng)常被用來實現(xiàn)原子性操作。另外請注意,synchronized 關(guān)鍵字會引起線程上下文切換并帶來線程調(diào)度開銷。

2.5 volatile

對于解決內(nèi)存可見性的問題,Java 還提供了一種弱形式的同步,也就是使用 volatile 關(guān)鍵字。該關(guān)鍵字可以確保對一個變量的更新對其他線程馬上可見。當(dāng)一個變量被聲明為 volatile 時,線程在寫入變量時不會把值緩存在寄存器或者其他地方,而是把值刷新回主內(nèi)存。當(dāng)其他線程讀取該共享變量時,會從主內(nèi)存重新獲取最新值,而不是使用當(dāng)前線程的工作內(nèi)存中的值。

2.6 Java 中的原子性操作

所謂原子性操作,是指執(zhí)行一系列操作時,這些操作要么全部執(zhí)行,要么全部不執(zhí)行,不存在只執(zhí)行其中一部分的情況。

線程安全性:即內(nèi)存可見性和原子性

2.7 Java 中的 CAS 操作

CAS 即 Compare and Swap,是 JDK 提供的非阻塞原子性操作,它通過硬件保證了比較 -- 更新操作的原子性。JDK 里面的 Unsafe 類提供了一系列的 compareAndSwap 方法。

// 比如說下面這個
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update);

其中 compareAndSwap 的意思是比較并交換。

CAS 有四個操作數(shù),分別為:對象內(nèi)存位置、對象中的變量的偏移量、變量預(yù)期值和新的值。其操作含義是,如果對象 obj 中內(nèi)存偏移量為 valueOffset 的變量值為 expect ,則使用新的值 update 替換舊的值 expect。這是處理器提供的一個原子性指令。

ABA 問題
CAS 操作有個經(jīng)典的 ABA 問題。

ABA 問題的產(chǎn)生是因為變量的狀態(tài)值產(chǎn)生了環(huán)形轉(zhuǎn)換,就是變量的值可以從 A 到 B,然后再從 B 到 A 。如果變量的值只能朝著一個方向轉(zhuǎn)換,比如 A 到 B,B 到 C,不構(gòu)成環(huán)形,就不會存在問題。JDK 中的 AtomicStampedReference 類給每個變量的狀態(tài)值都配備了一個時間戳,從而避免了 ABA 問題的產(chǎn)生。

2.8 Unsafe 類

  • JDK 的 rt.jar 包中的 Unsafe 類提供了硬件級別的原子性操作,Unsafe 類中的方法都是 native 方法,它們使用 JNI 的方式訪問本地 C++ 實現(xiàn)庫

2.9 Java 指令重排序

Java 內(nèi)存模型允許編譯器和處理器對指令重排序以提高運行性能,并且只會對不存在數(shù)據(jù)依賴性的指令重排序。在單線程下重排序可以保證最終執(zhí)行的結(jié)果與程序順序執(zhí)行的結(jié)果一致,但是在多線程下就會存在問題。

重排序在多線程下會導(dǎo)致非預(yù)期的程序執(zhí)行結(jié)果,而使用 volatile 修飾變量就可以避免重排序和內(nèi)存可見性問題。

寫 volatile 變量時,可以確保 volatile 寫之前的操作不會被編譯器重排序到 volatile 寫之后。讀 volatile 變量時,可以確保 volatile 讀之后的操作不會被編譯器重排序到 volatile 讀之前。

2.10 偽共享

2.10.1 什么是偽共享

為了解決主內(nèi)存與 CPU 之間運行速度差的問題,會在 CPU 與主內(nèi)存之間添加一級或多級高速緩沖器(Cache)。這個 Cache 一般是被集成到 CPU 內(nèi)部的,所以也叫 CPU Cache 。

在 Cache 內(nèi)部是按行存儲的,其中每一行稱為一個 Cache 行。Cache 行是 Cache 與主內(nèi)存進(jìn)行數(shù)據(jù)交換的單位。

由于存放到 Cache 行的是內(nèi)存塊而不是單個變量,所以可能會把多個變量存放到一個 Cache 行中。當(dāng)多個線程同時修改一個緩存行里面的多個變量時,由于同時只能有一個線程操作緩存行,所以相比將每一個變量放到一個緩存行,性能會有所下降,這就是偽共享。

2.10.2 如何避免偽共享

在 JDK 8 之前一般都是通過字節(jié)填充的方式來避免該問題,也就是創(chuàng)建一個變量時使用填充字段填充該變量所在的緩存行,這就避免了將多個變量存放在同一個緩存行中。

JDK 8 提供了一個 sun.misc.Contented 注解,用來解決偽共享問題。在默認(rèn)情況下,@Contented 注解只用于 Java 核心類,比如 rt 包下的類。如果用戶類路徑下的類需要使用這個注解,則需要添加 JVM 參數(shù):-XX:-RestrictContented 。

總結(jié)來說,在多線程下訪問同一個緩存行的多個變量時才會出現(xiàn)偽共享,在單線程下訪問一個緩存行里面的多個變量反而會對程序運行起到加速作用

2.11 鎖的概述

2.11.1 樂觀鎖與悲觀鎖
  • 悲觀鎖是指對數(shù)據(jù)被外界修改持保守態(tài)度,認(rèn)為數(shù)據(jù)很容易就會被其他線程修改,所以在數(shù)據(jù)被處理前先對數(shù)據(jù)進(jìn)行加鎖,并在整個數(shù)據(jù)處理過程中,使數(shù)據(jù)處于鎖定狀態(tài)
  • 樂觀鎖是相對悲觀鎖來說的,它認(rèn)為數(shù)據(jù)在一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在進(jìn)行數(shù)據(jù)提交更新時,才會對數(shù)據(jù)沖突與否進(jìn)行檢測
2.11.2 公平鎖與非公平鎖

根據(jù)線程獲取鎖的搶占機(jī)制,鎖可以分為 公平鎖 和 非公平鎖

  • 公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。

  • 非公平鎖則在運行時闖入,也就是先來不一定先得。
    ReentrantLock 提供了公平和非公平鎖的實現(xiàn)

  • 公平鎖:ReentrantLock pairLock = new ReentrantLock(true)

  • 非公平鎖:ReentrantLock pairLock = new ReentrantLock(false) ,默認(rèn)是非公平鎖

在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷

2.11.3 獨占鎖與共享鎖

根據(jù)鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為 獨占鎖 和 共享鎖 。

獨占鎖保證任何時候都只有一個線程能得到鎖,ReentrantLock 就是以獨占方式實現(xiàn)的。共享鎖則可以同時由多個線程持有,例如 ReadWriteLock 讀寫鎖,它允許一個資源可以被多個線程同時進(jìn)行讀操作。

  • 獨占鎖是一種悲觀鎖,由于每次訪問資源都先加上互斥鎖,這限制了并發(fā)性,因為讀操作并不會影響數(shù)據(jù)的一致性,而獨占鎖只允許在同一時間由一個線程讀取數(shù)據(jù),其他線程必須等待當(dāng)前線程釋放鎖才能進(jìn)行讀取
  • 共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進(jìn)行讀操作
2.11.4 可重入鎖
  • 當(dāng)一個線程要獲取一個被其他線程持有的獨占鎖時,該線程會被阻塞,那么當(dāng)一個線程再次獲取它自己已經(jīng)獲取的鎖時,如果不被阻塞,那么該鎖就是可重入的。
    synchronized 內(nèi)部鎖是可重入鎖。

可重入鎖的原理是在鎖內(nèi)部維護(hù)了一個線程標(biāo)示,用來標(biāo)示該鎖目前被哪個線程占用,然后關(guān)聯(lián)一個計數(shù)器,當(dāng)計數(shù)器值為 0 時說明該鎖沒有被任何線程占用,當(dāng)一個線程獲取了該鎖,計數(shù)器值會變?yōu)?1,這時其他線程再來獲取鎖時會發(fā)現(xiàn)鎖的所有者不是自己而被阻塞掛起。但是當(dāng)獲取了該鎖的線程再次獲取鎖時發(fā)現(xiàn)鎖擁有者是自己,計數(shù)器值就 + 1,當(dāng)釋放鎖后,計數(shù)器值 - 1。當(dāng)計數(shù)器值為 0 時,鎖里面的線程標(biāo)示被重置為 null ,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

2.11.5 自旋鎖

由于 Java 中的線程是與操作系統(tǒng)中的線程一一對應(yīng)的,所以當(dāng)一個線程在獲取鎖失敗后,會被切換到用戶態(tài)而被掛起。當(dāng)該線程獲取到鎖時又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程。而從用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的,在一定程度上會影響并發(fā)性能。

自旋鎖則是,當(dāng)前線程在獲取鎖時,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有,它不馬上阻塞自己,在不放棄 CPU 使用權(quán)的情況下,多次嘗試獲取(默認(rèn)次數(shù)是 10,可以使用 -XX:PreBlockSpinsh 參數(shù)設(shè)置該值),很有可能在后面幾次嘗試中其他線程已經(jīng)釋放了鎖。如果嘗試指定的次數(shù)后仍沒有獲取到鎖則當(dāng)前線程才會被阻塞掛起。

由此看來自旋鎖是使用 CPU 時間換取線程阻塞與調(diào)度的開銷,但是很有可能這些 CPU 時間白白浪費了。

3. ThreadLocalRandom

3.1 Random 類及其局限性

每個 Random 實例里面都有一個原子性的種子變量用來記錄當(dāng)前的種子值,當(dāng)要生成新的隨機(jī)數(shù)時需要根據(jù)當(dāng)前種子計算新的種子并更新會原子變量。當(dāng)多線程下使用單個 Random 實例生成隨機(jī)數(shù)時,當(dāng)多個線程同時計算隨機(jī)數(shù)來計算新的種子時,多個線程會競爭同一個原子變量的更新操作,由于原子變量的更新是 CAS 操作,同時只有一個線程會成功,所以會造成大量線程進(jìn)行自旋重試,這會降低并發(fā)性能,所以 ThreadLocalRandom 應(yīng)運而生。

3.2 ThreadLocalRandom

每個線程都維護(hù)一個種子變量,則每個線程生成隨機(jī)數(shù)時都根據(jù)自己老的種子計算新的種子,并使用新種子更新老的種子,再根據(jù)新種子計算隨機(jī)數(shù),就不會存在競爭問題了,這會大大提高并發(fā)性能。

ThreadLocalRandom 使用 ThreadLocal 的原理,讓每個線程都持有一個本地的種子變量,該種子變量只有在使用隨機(jī)數(shù)時才會被初始化。在多線程下計算新種子時是根據(jù)自己線程內(nèi)維護(hù)的種子變量進(jìn)行更新,從而避免了競爭。

4. JUC 中的原子操作類

JUC 包提供了一系列的原子性操作類,這些類都是使用非阻塞算法 CAS 實現(xiàn)的,相比使用鎖實現(xiàn)原子性操作這在性能上有很大提高。

4.1 AtomicLong

  • AtomicLong 是原子性遞增或遞減類,其內(nèi)部使用 Unsafe 來實現(xiàn)
    因為 AtomicLong 類是在 rt.jar 包下面的,AtomicLong 類就是通過 BootStarp 類加載器進(jìn)行加載的,所以其內(nèi)部實現(xiàn)時可以直接通過 Unsafe.getUnsafe() 方法獲取到 Unsafe 類的實例

在高并發(fā)情況下 AtomicLong 還會存在性能問題。JDK 8 提供了一個在高并發(fā)下性能更好的 LongAdder 類

使用 AtomicLong 時,在高并發(fā)下大量線程會同時去競爭更新同一個原子變量,但是由于同時只有一個線程的 CAS 操作會成功,這就造成了大量線程競爭失敗后,會通過無限循環(huán)不斷進(jìn)行自旋嘗試 CAS 的操作,而這會白白浪費 CPU 資源。

4.2 LongAdder

為了解決高并發(fā)下多線程對一個變量 CAS 爭奪失敗后進(jìn)行自旋而造成的降低并發(fā)性能的問題,LongAdder 在內(nèi)部維護(hù)多個 Cell 元素(一個動態(tài) Cell 數(shù)組)來分擔(dān)對單個變量進(jìn)行爭奪的開銷,每個 Cell 里面有一個初始值為 0 的 long 型變量,這樣,在同等并發(fā)量的情況下,爭奪單個變量更新操作的線程量會減少。

另外,多個線程在爭奪同一個 Cell 原子變量時如果失敗了,它并不是在當(dāng)前 Cell 變量上一直自旋 CAS 重試,而是嘗試在其他 Cell 的變量上進(jìn)行 CAS 嘗試,這個改變增加了當(dāng)前線程重試 CAS 成功的可能性。

最后,在獲取 LongAdder 當(dāng)前值時,是把所有 Cell 變量的 value 值累加后再加上 base 返回的。

由于 Cells 占用的內(nèi)存是相對較大的,所以一開始并不創(chuàng)建它,而是在需要時創(chuàng)建,也就是 惰性加載 。
另外,數(shù)組元素 Cell 使用 @sun.misc.Contented 注解進(jìn)行修飾,這避免了 Cells 數(shù)組內(nèi)多個原子變量被放入同一個緩存行,也就是避免了 偽共享,這對性能也是一個提升。

LongAccumulator
LongAdder 類是 LongAccumulator 的一個特例,只是后者提供了更加強(qiáng)大的功能,可以讓用戶自定義規(guī)則。

5. CopyOnWriteArrayList

并發(fā)包中的并發(fā) list 只有 CopyOnWriteArrayList,它是無界 list 。

CopyOnWriteArrayList 使用寫時復(fù)制的策略來保證 list 的一致性,而 獲取 - 修改 - 寫入 三步操作并不是原子性的,所以在增刪改的過程中都使用了獨占鎖,來保證在某個時間只有一個線程能對 list 數(shù)組進(jìn)行修改。另外 CopyOnWriteArrayList 提供了弱一致性的迭代器,從而保證在獲取迭代器后,其他線程對 list 的修改是不可見的,迭代器遍歷的數(shù)組是一個快照。另外,CopyOnWriteArraySet 的底層就是使用它實現(xiàn)的。

6. JUC中鎖原理

6.1 LockSupport

  • LockSupport 是個工具類,它的主要作用是掛起和喚醒線程,該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)。
  • LockSupport 類與每個使用它的線程都會關(guān)聯(lián)一個許可證,在默認(rèn)情況下調(diào)用 LockSupport 類的方法的線程是不持有許可證的。LockSupport 是使用 Unsafe 類實現(xiàn)的。
6.1.1 void park()
  • 如果調(diào)用 park 方法的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 時會馬上返回,否則調(diào)用線程會被禁止參與線程的調(diào)度,也就是會被阻塞掛起。
6.1.2 void unpark()
  • 當(dāng)一個線程調(diào)用 unpark 時,如果參數(shù) thread 線程沒有持有 thread 與 LockSupport 類關(guān)聯(lián)的許可證,則讓 thread 線程持有。
  • 如果 thread 之前因調(diào)用 park() 而被掛起,則調(diào)用 unpark() 后,該線程會被喚醒。
  • 如果 thread 之前沒有調(diào)用 park(),則調(diào)用 unpark 方法后,再調(diào)用 park 方法,會立刻返回。
6.1.3 其他方法

1.void parkNanos(long nanos)
2.park(Object blocker)
3.void parkNanos(Object blocker, long nanos)
4.void parkUntil(Object blocker, long deadline)

6.2 AQS

  • AbstractQueuedSynchronizer 抽象同步隊列簡稱 AQS,它是實現(xiàn)同步器的基礎(chǔ)組件,并發(fā)包中鎖的底層就是使用 AQS 實現(xiàn)
  • AQS 是一個 FIFO 的雙向隊列,其內(nèi)部通過節(jié)點 head 和 tail 記錄隊首和隊尾元素,隊列元素的類型為 Node。其中 Node 中的 thread 變量用來存放進(jìn)入 AQS 隊列里的線程
  • 在 AQS 中維持了一個單一的狀態(tài)信息 state,可以通過 getState、setState、compareAndSetState 函數(shù)修改其值。
  • AQS 有個內(nèi)部類 ConditionObject,用來結(jié)合鎖實現(xiàn)線程同步。
  • 對于 AQS 來說,線程同步的關(guān)鍵是對狀態(tài)值 state 進(jìn)行操作。

6.2.1 條件變量的支持

notify 和 wait ,是配合 synchronized 內(nèi)置鎖實現(xiàn)線程間同步的基礎(chǔ)設(shè)施一樣,條件變量的 signal 和 await 方法也是用來配合鎖(使用 AQS 實現(xiàn)的鎖)實現(xiàn)線程間同步的基礎(chǔ)設(shè)施。

它們的不同在于,synchronized 同時只能與一個共享變量的 notify 或 wait 方法實現(xiàn)同步,而 AQS 的一個鎖可以對應(yīng)多個條件變量。

ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

lock.newCondition() 的作用其實是 new 了一個在 AQS 內(nèi)部聲明的 ConditionObject 對象,ConditionObject 是 AQS 的內(nèi)部類,可以訪問 AQS 內(nèi)部的變量(例如狀態(tài)變量 state)和方法。在每個條件變量內(nèi)部都維護(hù)了一個條件隊列,用來存放調(diào)用條件變量的 await() 方法時被阻塞的線程。注意這個條件隊列和 AQS 隊列不是一回事。

注意不要混淆 AQS 阻塞隊列與條件變量隊列:

  • 當(dāng)多個線程同時調(diào)用 lock.lock() 方法獲取鎖時,只有一個線程獲取到了鎖,其他線程會被轉(zhuǎn)換為 Node 節(jié)點插入到 lock 鎖對應(yīng)的 AQS 阻塞隊列里面,并做自旋 CAS 嘗試獲取鎖。
  • 如果獲取到鎖的線程又調(diào)用了對應(yīng)的條件變量的 await() 方法,則該線程會釋放獲取到的鎖,并被轉(zhuǎn)換為 Node 節(jié)點插入到條件變量對應(yīng)的條件隊列里面。
  • 這時候因為調(diào)用 lock.lock() 方法被阻塞到 AQS 隊列里面的一個線程會獲取到被釋放的鎖,如果該線程也調(diào)用了條件變量的 await() 方法則該線程也會被放入條件變量的條件隊列里面。
  • 當(dāng)另外一個線程調(diào)用條件變量的 signal() 或者 signalAll() 方法時,會把條件隊列里面的一個或者全部 Node 節(jié)點移動到 AQS 的阻塞隊列里面,等待時機(jī)獲取鎖。

也就是說,一個鎖對應(yīng)一個 AQS 阻塞隊列,對應(yīng)多個條件變量,每個條件變量有自己的一個條件隊列。

6.3 獨占鎖 ReentrantLock

  • ReentrantLock 是可重入的獨占鎖,同時只能有一個線程可以獲取該鎖,其他獲取該鎖的線程會被阻塞而被放入該鎖的 AQS 阻塞隊列里面。

6.3.1 獲取鎖

void lock()

  • 調(diào)用該方法時,如果鎖當(dāng)前沒有被其他線程占用并且當(dāng)前線程之前沒有獲取過該鎖,則當(dāng)前線程會獲取到該鎖,然后設(shè)置當(dāng)前鎖的擁有者為當(dāng)前線程,并設(shè)置 AQS 的狀態(tài)值 state 為 1,然后直接返回。如果當(dāng)前線程之前已經(jīng)獲取過該鎖,則這次只是簡單的把 AQS 的狀態(tài)值加 1 后返回。如果該鎖已經(jīng)被其他線程持有,則調(diào)用該方法的線程會被放入 AQS 隊列后阻塞掛起。

當(dāng)然還有其他的獲取鎖的方法

  • void lockInterruptibly():對中斷進(jìn)行響應(yīng)
  • boolean tryLock():嘗試獲取鎖,如果當(dāng)前該鎖沒有被其他線程持有,則當(dāng)前線程獲取該鎖并返回 true,否則返回 false。注意,該方法不會引起當(dāng)前線程阻塞。

6.3.2 釋放鎖

void unlock()

  • 嘗試釋放鎖,如果當(dāng)前線程持有該鎖,則調(diào)用該方法會讓線程對該線程持有的 AQS 狀態(tài)值減 1,如果減去 1 后當(dāng)前狀態(tài)值為 0 ,則當(dāng)前線程會釋放該鎖,否則僅僅減 1 而已。如果當(dāng)前線程沒有持有該鎖而調(diào)用了該方法則會拋出 IllegalMonitorStateException 異常。

總的來說,ReentrantLock 的底層是使用 AQS 實現(xiàn)的可重入獨占鎖。在這里 AQS 狀態(tài)值為 0 表示當(dāng)前鎖空閑,為大于等于 1 的值則說明該鎖已經(jīng)被占用。該鎖內(nèi)部有公平與非公平實現(xiàn),默認(rèn)情況下是非公平的實現(xiàn)。

6.4 讀寫鎖 ReentrantReadWriteLock

ReentrantReadWriteLock 的底層是使用 AQS 實現(xiàn)的。ReentrantReadWriteLock 巧妙的使用 AQS 的狀態(tài)值的高 16 位表示獲取到讀鎖的個數(shù),低 16 位表示獲取寫鎖的線程的可重入次數(shù),并通過 CAS 對其進(jìn)行操作實現(xiàn)了讀寫分離,這在讀多寫少的場景下比較適用。

6.5 StampedLock

StampedLock 是并發(fā)包里面 JDK8 版本新增的一個類,該鎖提供了三種模式的讀寫控制,當(dāng)調(diào)用獲取鎖系列函數(shù)時,會返回一個 long 型的變量,我們稱之為 戳記(stamp),這個戳記代表了鎖的狀態(tài)。其中 try 系列獲取鎖的函數(shù),當(dāng)獲取鎖失敗后會返回為 0 的 stamp值。當(dāng)調(diào)用釋放鎖和轉(zhuǎn)換鎖的方法時需要傳入獲取鎖時返回的 stamp 值。

StampedLock 提供的三種讀寫模式的鎖:

  • 寫鎖 writeLock:獨占鎖,不可重入
  • 悲觀讀鎖 readLock:共享鎖,不可重入
  • 樂觀讀鎖 tryOptimisticRead:只是使用位操作進(jìn)行檢驗,不涉及 CAS 操作,所以效率會高很多

StampedLock 提供的讀寫鎖與 ReentrantReadWriteLock 類似,只是前者提供的是不可重入鎖。但是前者通過提供樂觀讀鎖在多線程多讀的情況下提供了更好的性能,這是因為獲取樂觀讀鎖時不需要進(jìn)行 CAS 操作設(shè)置鎖的狀態(tài),而只是簡單的測試狀態(tài)。

7. Java 并發(fā)包中的并發(fā)隊列

7.1 ConcurrentLinkedQueue

  • ConcurrentLinkedQueue 是線程安全的無界非阻塞隊列,其底層數(shù)據(jù)結(jié)構(gòu)使用單向鏈表實現(xiàn),對于入隊和出隊操作使用 CAS 來實現(xiàn)線程安全。


7.2 LinkedBlockingQueue

  • LinkedBlockingQueue 也是使用單向鏈表實現(xiàn)的,其也有兩個 Node ,分別用來存放首、尾節(jié)點,并且還有一個初始值為 0 的原子變量 count ,用來記錄隊列元素個數(shù)。
  • 另外還有兩個 ReentrantLock 的實例,分別用來控制元素入隊和出隊的原子性,其中 takeLock 用來控制同時只有一個線程可以從隊列頭獲取元素,其他線程必須等待;putLock 控制同時只能有一個線程可以獲取鎖,在隊列尾部添加元素,其他線程必須等待。
  • 另外,notEmpty 和 notFull 是條件變量,它們內(nèi)部都有一個條件隊列用來存放進(jìn)隊和出隊時被阻塞的線程,其實這是 生產(chǎn)者-消費者 模型。
  • LinkedBlockingQueue 默認(rèn)隊列容量為 0x7fffffff,用戶也可以自己指定容量,所以從一定程度上可以說 LinkedBlockingQueue 是有界阻塞隊列。


7.3 ArrayBlockingQueue

LinkedBlockingQueue 是基于有界鏈表方式實現(xiàn)的阻塞隊列,而 ArrayBlockingQueue 是基于基于有界數(shù)組實現(xiàn)的阻塞隊列。

ArrayBlockingQueue 的內(nèi)部有一個數(shù)組 items ,用來存放隊列元素,putIndex 變量表示入隊元素下標(biāo),takeIndex 是出隊下標(biāo),count 統(tǒng)計隊列元素個數(shù)。另外,有個獨占鎖 lock 用來保證出、入隊操作的原子性,這保證了同時只有一個線程可以進(jìn)行入隊、出隊操作。另外,notEmpty、notFull 條件變量用來進(jìn)行出、入隊的同步。

ArrayBlockingQueue 是有界隊列,所以構(gòu)造函數(shù)必須傳入隊列大小參數(shù)。


7.4 PriorityBlockingQueue

PriorityBlockingQueue 是帶優(yōu)先級的無界阻塞隊列,每次出隊都返回優(yōu)先級最高或者最低的元素。其內(nèi)部是使用平衡二叉樹堆實現(xiàn)的,所以直接遍歷隊列元素不保證有序。

PriorityBlockingQueue 隊列在內(nèi)部使用二叉樹堆維護(hù)元素優(yōu)先級,使用數(shù)組作為元素存儲的數(shù)據(jù)結(jié)構(gòu),這個數(shù)組是可擴(kuò)容的。當(dāng)當(dāng)前元素個數(shù) >= 最大容量時會通過 CAS 算法擴(kuò)容,出隊時始終保證出隊的元素是堆樹的根節(jié)點,而不是在隊列里面停留時間最長的元素。使用元素的 compareTo 方法提供默認(rèn)的元素優(yōu)先級比較規(guī)則,用戶可以自定義優(yōu)先級的比較規(guī)則。


7.5 DelayQueue

DelayQueue 并發(fā)隊列是一個無界阻塞延遲隊列,隊列中的每個元素都有個過期時間,當(dāng)從隊列獲取元素時,只有過期元素才會出隊列。隊頭元素是最快要過期的隊列。
DelayQueue 內(nèi)部使用 PriorityQueue 存放數(shù)據(jù),使用 ReentrantLock 實現(xiàn)線程同步。另外隊列里面的元素要實現(xiàn) Delayed 接口,其中一個是獲取當(dāng)前元素到過期時間剩余時間的接口,在出隊時判斷元素是否過期了,一個是元素之間比較的接口,因為這是一個有優(yōu)先級的隊列。


8. ThreadPoolExecutor

8.1 介紹

線程池主要解決兩個問題:一是當(dāng)執(zhí)行大量異步任務(wù)時線程池能夠提供較好的性能。在不使用線程池時,每當(dāng)需要執(zhí)行異步任務(wù)時直接 new 一個線程來運行,而線程的創(chuàng)建和銷毀是需要開銷的。線程池里面的線程是可復(fù)用的,不需要每次執(zhí)行異步任務(wù)時都重新創(chuàng)建和銷毀線程。二是線程池提供了一種 資源限制 和 管理 的手段,比如可以限制線程的個數(shù),動態(tài)新增線程等。每個 ThreadPoolExecutor 也保留了一些基本的統(tǒng)計數(shù)據(jù),比如當(dāng)前線程池完成的任務(wù)數(shù)目等。

另外,線程池也提供了許多可調(diào)參數(shù)和可擴(kuò)展性接口,以滿足不同情景的需要,程序員可以使用更方便的 Executors 的工廠方法,比如 newCachedThreadPool(線程池線程個數(shù)最多可達(dá) Integer.MAX_VALUE,線程自動回收)、newFixedThreadPool(固定大小的線程池)和 newSingleThreadExecutor(單個線程)等來創(chuàng)建線程池,當(dāng)然用戶還可以自定義。

線程池參數(shù)

  • corePoolSize:線程池核心線程個數(shù)。
  • workQueue:用于保存等待執(zhí)行的任務(wù)的阻塞隊列,比如基于數(shù)組的有界 ArrayBlockingQueue、基于鏈表的無界 LinkedBlockingQueue、最多只有一個元素的同步隊列 SynchronousQueue 及優(yōu)先級隊列 PriorityBlockingQueue 等。
  • maximunPoolSize:線程池最大線程數(shù)量。
  • ThreadFactory:創(chuàng)建線程的工廠。
  • RejectedExecutionHandler:飽和策略,當(dāng)隊列滿并且線程個數(shù)達(dá)到 maximunPoolSize 后采取的策略,比如 AbortPolicy(拋出異常)、CallerRunsPolicy(使用調(diào)用者所在線程來運行任務(wù))、DiscardOldestPolicy(調(diào)用 poll 丟棄一個任務(wù),執(zhí)行當(dāng)前任務(wù))及 DiscardPolicy(默默丟棄,不拋出異常)。
  • keepAliveTime:存活時間。如果當(dāng)前線程池中的線程數(shù)量比核心線程數(shù)量多,并且是閑置狀態(tài),則這些閑置的線程能存活的最大時間。
  • TimeUnit:存活時間的時間單位。

線程池類型

  • newFixedThreadPool:創(chuàng)建一個核心線程個數(shù)和最大線程個數(shù)都為 nThreads 的線程池,并且阻塞隊列長度為 Integer.MAX_VALUE。keepAliveTime = 0 說明只要線程個數(shù)比核心線程個數(shù)多并且當(dāng)前空閑則回收。
  • newSingleThreadExecutor:創(chuàng)建一個核心線程個數(shù)和最大線程個數(shù)都為 1 的線程池,并且阻塞隊列長度為 Integer.MAX_VALUE。keepAliveTime = 0 說明只要線程個數(shù)比核心線程個數(shù)多并且當(dāng)前空閑則回收。
  • newCachedThreadPool:創(chuàng)建一個按需創(chuàng)建線程的線程池,初始線程個數(shù)為 0 ,最多線程個數(shù)為 Integer.MAX_VALUE,并且阻塞隊列為同步隊列。keepAliveTime = 60 說明只要當(dāng)前線程在 60s 內(nèi)空閑則回收。這個類型的特殊之處在于,加入同步隊列的任務(wù)會被馬上執(zhí)行,同步隊列里面最多只有一個任務(wù)。

public void execute(Runnable command)

  • execute() 方法的作用是提交任務(wù) command 到線程池進(jìn)行執(zhí)行


  • 從圖中可以看出,ThreadPoolExecutor 的實現(xiàn)實際上是一個生產(chǎn)消費模型,當(dāng)用戶添加任務(wù)到線程池時相當(dāng)于生產(chǎn)者生產(chǎn)元素,worker 線程工作集中的線程直接執(zhí)行任務(wù)或者從任務(wù)隊列里面獲取任務(wù)時則相當(dāng)于消費者消費元素。

總結(jié):線程池巧妙的使用一個 Integer 類型的原子變量來記錄線程池狀態(tài)和線程池中的線程個數(shù)。通過線程池狀態(tài)來控制任務(wù)的執(zhí)行,每個 Worker 線程可以處理多個任務(wù)。線程池通過線程的復(fù)用減少了線程創(chuàng)建和銷毀的開銷。

9. ScheduledThreadPoolExecutor

Executor 其實是個工具類,它提供了好多靜態(tài)方法,可根據(jù)用戶的選擇返回不同的線程池實例。ScheduledThreadPoolExecutor 繼承了 ThreadPoolExecutor 并實現(xiàn)了 ScheduledExecutorService 接口。線程池隊列是 DelayedWorkQueue,其和 DelayedQueue 類似,是一個延遲隊列。

9.3.1 schedule(Runnable command, long delay, TimeUnit unit)
該方法的作用是提交一個延遲執(zhí)行的任務(wù),任務(wù)從提交時間算起延遲單位為 unit 的 delay 時間后開始執(zhí)行。提交的任務(wù)不是周期性任務(wù),任務(wù)只會執(zhí)行一次。

9.3.2 scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
fixed-delay 類型的任務(wù)的執(zhí)行原理為,當(dāng)添加一個任務(wù)到延遲隊列后,等待 initialDelay 時間,任務(wù)就會過期,過期的任務(wù)就會被從隊列移除,并執(zhí)行。執(zhí)行完畢后,會重新設(shè)置任務(wù)的延遲時間,然后再把任務(wù)放入延遲隊列,循環(huán)往復(fù)。需要注意的是,如果一個任務(wù)在執(zhí)行中拋出了異常,那么這個任務(wù)就結(jié)束了,但是不影響其他任務(wù)的執(zhí)行。

9.3.3 scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
相對于 fixed-delay 任務(wù)來說,fixed-rate 方式執(zhí)行規(guī)則為,時間為 initdelday + n * period 時啟動任務(wù),但是如果當(dāng)前任務(wù)還沒有執(zhí)行完,下一次要執(zhí)行的時間到了,則不會并發(fā)執(zhí)行,下次要執(zhí)行的任務(wù)會延遲執(zhí)行,要等到當(dāng)前任務(wù)執(zhí)行完畢后再執(zhí)行。

總結(jié):其內(nèi)部使用 DelayQueue 來存放具體任務(wù)。任務(wù)分為三種,其中一次性執(zhí)行任務(wù)執(zhí)行完畢就結(jié)束了,fixed-delay 任務(wù)保證同一個任務(wù)在多次執(zhí)行之間間隔固定時間,fixed-rate 任務(wù)保證按照固定的頻率執(zhí)行。任務(wù)類型使用 period 的值來區(qū)分。

10. Java 并發(fā)包中線程同步器--線程協(xié)作

10.1 CountDownLatch

10.1.1 CountDownLatch 與 join 方法的區(qū)別

一個區(qū)別是,調(diào)用一個子線程的 join() 方法后,該線程會一直被阻塞直到子線程運行完畢,而 CountDownLatch 則使用計數(shù)器來允許子線程運行完畢或者在運行中遞減計數(shù),也就是 CountDownLatch 可以在子線程運行的任何時候讓 await() 方法返回而不一定必須等到線程結(jié)束。

另外,使用線程池來管理線程時一般都是直接添加 Runnable 到線程池,這時候就沒有辦法再調(diào)用線程的 join() 方法了,就是說 CountDownLatch 相比 join() 方法讓我們對線程的同步有更靈活的控制。

10.1.2 原理

CountDownLatch 是使用 AQS 實現(xiàn)的,使用 AQS 的狀態(tài)變量來存放計數(shù)器的值。首先在初始化 CountDownLatch 時設(shè)置狀態(tài)值(計數(shù)器值),當(dāng)多個線程調(diào)用 countDown() 方法時實際是原子性遞減 AQS 的狀態(tài)值。當(dāng)線程調(diào)用 await() 方法后當(dāng)前線程會被放入 AQS 的阻塞隊列等待計數(shù)器為 0 再返回。其他線程調(diào)用 countDown() 方法讓計數(shù)器值遞減 1,當(dāng)計數(shù)器值變?yōu)?0 時,當(dāng)前線程還要調(diào)用 AQS 的 doReleaseShared 方法來激活由于調(diào)用 await() 方法而被阻塞的線程。

10.2 回環(huán)屏障 CyclicBarrier

CountDownLatch 在解決多個線程同步方面相對于調(diào)用線程的 join() 方法已經(jīng)有了不少優(yōu)化,但是 CountDowmLatch 的計數(shù)器是一次性的,也就是等到計數(shù)器值變?yōu)?0 后,再調(diào)用 CountDownLatch 的 await() 和 countDown() 方法都會立刻返回,這就起不到線程同步的效果了。

所以,為了滿足計數(shù)器可以重置的需要,JDK 開發(fā)組提供了 CyclicBarrier 類,并且 Cyclicbarrier 類的功能并不限于 CountDownLatch 的功能。

從字面意思理解,CyclicBarrier 是回環(huán)屏障的意思,它可以讓一組線程全部達(dá)到一個狀態(tài)后再全部同時執(zhí)行。這里之所以叫做回環(huán)是因為當(dāng)所有等待線程執(zhí)行完畢,并重置 CyclicBarrier 的狀態(tài)后它可以被重用。之所以叫做屏障是因為線程調(diào)用 await 方法后就會被阻塞,這個阻塞點就稱為屏障點,等所有線程都調(diào)用了 await 方法后,線程們就會沖破屏障,繼續(xù)向下運行。

  • CyclicBarrier 與 CountDowmLatch 的不同在于,前者是可以復(fù)用的,并且前者特別適合分段任務(wù)有序執(zhí)行的場景。
  • CyclicBarrier 通過 ReentrantLock 實現(xiàn)計數(shù)器原子性更新,并使用條件變量隊列來實現(xiàn)線程同步。

10.3 信號量 Semaphore

Semaphore 信號量也是 Java 中的一個同步器,與 CountDownLatch 和 CyclicBarrier 不同的是,它內(nèi)部的計數(shù)器是遞增的,并且在一開始初始化 Semphore 時可以指定一個初始值,但是并不需要知道需要同步的線程個數(shù),而是在需要同步的地方調(diào)用 acquire() 方法時指定需要同步的線程個數(shù)。

Semaphore 完全可以達(dá)到 CountDownLatch 的效果,但是 Semaphore 的計數(shù)器是不可以自動重置的,不過通過變相的改變 aquire() 方法的參數(shù)還是可以實現(xiàn) CyclicBarrier 的功能的。
Semaphore 也是使用 AQS 實現(xiàn)的,并且獲取信號量時有公平策略和非公平策略之分。

11. 并發(fā)編程實踐--一些注意事項

11.1 ArrayBlockingQueue

需要注意 put 、offer 方法的使用場景以及它們之間的區(qū)別,take 方法的使用,也需要注意使用 ArrayBlockingQueue 時需要設(shè)置合理的隊列大小以避免 OOM,隊列滿或者剩余元素比較少時,要根據(jù)具體場景制定一些拋棄策略以避免隊列滿時業(yè)務(wù)線程被阻塞。

  • put() 方法是阻塞的,也就是說如果當(dāng)前隊列滿,則在調(diào)用 put 方法向隊列放入一個元素時調(diào)用線程會被阻塞直到隊列有空余空間。
  • offer() 方法是非阻塞的,如果當(dāng)前隊列滿,則會直接返回,也就是丟棄當(dāng)前元素。
  • pool() 方法是從隊列頭部獲取并移除一個元素,如果隊列為空則返回 null ,該方法是不阻塞的
  • take() 方法是獲取當(dāng)前隊列頭部元素并從隊列里面移除它。如果隊列為空則阻塞當(dāng)前線程直到隊列不為空然后返回元素。

11.2 ConcurrentHashMap

put(K key, V value) 方法判斷如果 key 已經(jīng)存在,則使用 value 覆蓋原來的值并返回原來的值,如果不存在則把 value 放入并返回 null。

而 putIfAbsent(K key, V value) 方法則是如果 key 已經(jīng)存在則直接返回原來對應(yīng)的值并不使用 value 覆蓋,如果 key 不存在則放入 value 并返回 null ,另外要注意,判斷 key 是否存在和放入是原子性操作。

11.3 SimpleDateFormat

多線程共用一個 SimpleDateFormat 實例對日期進(jìn)行解析或格式化會導(dǎo)致程序出錯,因為在內(nèi)部實現(xiàn)中,其操作步驟不是原子性的,比如說重置日期對象屬性值與使用解析好的屬性性設(shè)置日期對象是兩個步驟,所以在多線程環(huán)境下使用同一個 SimpleDateFormat 實例會導(dǎo)致程序錯誤。

那如何解決呢?

1.第一種方式:每次使用時都 new 一個 SimpleDateFormat 的實例,這樣可以保證每個實例使用自己的 Calender 實例,但是每次使用都 new 一個對象,并且使用后由于沒有其他引用,又需要回收,開銷會很大。
2.第二種方式:出錯的原因在于其內(nèi)部實現(xiàn)中步驟不是一個原子性操作,我們可以使用 synchronized 進(jìn)行同步,這意味著多個線程要競爭鎖,在高并發(fā)場景下會導(dǎo)致系統(tǒng)響應(yīng)性能下降。
3.第三種方式:使用 ThreadLocal,這樣每個線程只需要使用一個 SimpleDateFormat 實例,這相比第一種方式大大節(jié)省了對象的創(chuàng)建銷毀開銷,并且不需要使用多個線程同步。但要注意,使用完線程變量后,要進(jìn)行清理(remove()),以避免內(nèi)存泄漏。

11.4 Timer

當(dāng)一個 Timer 運行多個 TimerTask 時,只要其中一個 TimerTask 在執(zhí)行中向 run 方法外拋出了異常,則其他任務(wù)也會自動終止。

ScheduledThreadPoolExecutor 是并發(fā)包提供的組件,其提供的功能包含但不限于 Timer。Timer 是固定的多線程生產(chǎn)單線程消費,但是 ScheduledThreadPoolExecutor 是可以配置的,既可以是多線程生產(chǎn)多線程消費也可以是多線程生產(chǎn)多線程消費,所以在日常開發(fā)中使用定時器功能時應(yīng)該優(yōu)先使用 ScheduledThreadPoolExecutor。

11.5 創(chuàng)建線程和線程池時要指定與業(yè)務(wù)相關(guān)的名稱

在日常開發(fā)中,當(dāng)在一個應(yīng)用中需要創(chuàng)建多個線程或者線程池時最好給每個線程或線程池根據(jù)業(yè)務(wù)類型設(shè)置具體的名稱,以便在出現(xiàn)問題時方便進(jìn)行定位。

另外,在使用線程池的情況下當(dāng)程序結(jié)束時一定要記得調(diào)用 shutdown() 關(guān)閉線程池

11.6 有關(guān) FutureTask

在線程池中使用 FutureTask 時,當(dāng)拒絕策略為 DiscardPolicy 和 DiscardOldestPolicy 時,在被拒絕的任務(wù)的 FutureTask 對象上調(diào)用 get() 方法會導(dǎo)致調(diào)用線程一直阻塞,所以在日常開發(fā)中盡量使用帶超時參數(shù)的 get() 方法以避免線程一直阻塞。

11.7 有關(guān)ThreadLocal

在線程中使用完 ThreadLocal 變量后,要及時調(diào)用 remove() 方法以避免內(nèi)存泄漏。

更多實踐內(nèi)容請參考我的文集:《J2SE-并發(fā)編程》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容