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

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

1.1 什么是線程

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

操作系統(tǒng)在分配資源時(shí)是把資源分配給進(jìn)程的,但是 CPU 資源比較特殊,它是被分配到線程的,因?yàn)橐嬲加?CPU 運(yùn)行的是線程,所以也說(shuō)線程是 CPU 分配的基本單位

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

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

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

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

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

1.3 線程通知與等待

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

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

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

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

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

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

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

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

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

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

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

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

1.5 讓線程睡眠的 sleep 方法

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

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

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

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

1.7 線程中斷

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

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

1.8 線程上下文切換

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

1.9 線程死鎖

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

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

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

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

資源的有序分配會(huì)避免死鎖,因?yàn)橘Y源的有序性破壞了資源的請(qǐng)求并持有條件和環(huán)路等待條件,因此避免了死鎖。

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

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

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

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

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

1.11 ThreadLocal

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

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

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

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

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

2.2 Java 中的線程安全問題

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

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

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

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

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

2.4 synchronized

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

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

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

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

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

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

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

2.5 volatile

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

2.6 Java 中的原子性操作

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

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

2.7 Java 中的 CAS 操作

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

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

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

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

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

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

2.8 Unsafe 類

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

2.9 Java 指令重排序

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

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

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

2.10 偽共享

2.10.1 什么是偽共享

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

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

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

2.10.2 如何避免偽共享

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

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

總結(jié)來(lái)說(shuō),在多線程下訪問同一個(gè)緩存行的多個(gè)變量時(shí)才會(huì)出現(xiàn)偽共享,在單線程下訪問一個(gè)緩存行里面的多個(gè)變量反而會(huì)對(duì)程序運(yùn)行起到加速作用

2.11 鎖的概述

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

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

  • 公平鎖表示線程獲取鎖的順序是按照線程請(qǐng)求鎖的時(shí)間早晚來(lái)決定的,也就是最早請(qǐng)求鎖的線程將最早獲取到鎖。

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

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

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

在沒有公平性需求的前提下盡量使用非公平鎖,因?yàn)楣芥i會(huì)帶來(lái)性能開銷

2.11.3 獨(dú)占鎖與共享鎖

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

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

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

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

2.11.5 自旋鎖

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

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

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

3. ThreadLocalRandom

3.1 Random 類及其局限性

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

3.2 ThreadLocalRandom

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

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

4. JUC 中的原子操作類

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

4.1 AtomicLong

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

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

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

4.2 LongAdder

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

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

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

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

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

5. CopyOnWriteArrayList

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

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

6. JUC中鎖原理

6.1 LockSupport

  • LockSupport 是個(gè)工具類,它的主要作用是掛起和喚醒線程,該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)。
  • LockSupport 類與每個(gè)使用它的線程都會(huì)關(guān)聯(lián)一個(gè)許可證,在默認(rèn)情況下調(diào)用 LockSupport 類的方法的線程是不持有許可證的。LockSupport 是使用 Unsafe 類實(shí)現(xiàn)的。
6.1.1 void park()
  • 如果調(diào)用 park 方法的線程已經(jīng)拿到了與 LockSupport 關(guān)聯(lián)的許可證,則調(diào)用 LockSupport.park() 時(shí)會(huì)馬上返回,否則調(diào)用線程會(huì)被禁止參與線程的調(diào)度,也就是會(huì)被阻塞掛起。
6.1.2 void unpark()
  • 當(dāng)一個(gè)線程調(diào)用 unpark 時(shí),如果參數(shù) thread 線程沒有持有 thread 與 LockSupport 類關(guān)聯(lián)的許可證,則讓 thread 線程持有。
  • 如果 thread 之前因調(diào)用 park() 而被掛起,則調(diào)用 unpark() 后,該線程會(huì)被喚醒。
  • 如果 thread 之前沒有調(diào)用 park(),則調(diào)用 unpark 方法后,再調(diào)用 park 方法,會(huì)立刻返回。
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 抽象同步隊(duì)列簡(jiǎn)稱 AQS,它是實(shí)現(xiàn)同步器的基礎(chǔ)組件,并發(fā)包中鎖的底層就是使用 AQS 實(shí)現(xiàn)
  • AQS 是一個(gè) FIFO 的雙向隊(duì)列,其內(nèi)部通過節(jié)點(diǎn) head 和 tail 記錄隊(duì)首和隊(duì)尾元素,隊(duì)列元素的類型為 Node。其中 Node 中的 thread 變量用來(lái)存放進(jìn)入 AQS 隊(duì)列里的線程
  • 在 AQS 中維持了一個(gè)單一的狀態(tài)信息 state,可以通過 getState、setState、compareAndSetState 函數(shù)修改其值。
  • AQS 有個(gè)內(nèi)部類 ConditionObject,用來(lái)結(jié)合鎖實(shí)現(xiàn)線程同步。
  • 對(duì)于 AQS 來(lái)說(shuō),線程同步的關(guān)鍵是對(duì)狀態(tài)值 state 進(jìn)行操作。

6.2.1 條件變量的支持

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

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

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

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

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

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

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

6.3 獨(dú)占鎖 ReentrantLock

  • ReentrantLock 是可重入的獨(dú)占鎖,同時(shí)只能有一個(gè)線程可以獲取該鎖,其他獲取該鎖的線程會(huì)被阻塞而被放入該鎖的 AQS 阻塞隊(duì)列里面。

6.3.1 獲取鎖

void lock()

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

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

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

6.3.2 釋放鎖

void unlock()

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

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

6.4 讀寫鎖 ReentrantReadWriteLock

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

6.5 StampedLock

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

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

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

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

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

7.1 ConcurrentLinkedQueue

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


7.2 LinkedBlockingQueue

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


7.3 ArrayBlockingQueue

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

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

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


7.4 PriorityBlockingQueue

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

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


7.5 DelayQueue

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


8. ThreadPoolExecutor

8.1 介紹

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

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

線程池參數(shù)

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

線程池類型

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

public void execute(Runnable command)

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


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

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

9. ScheduledThreadPoolExecutor

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

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

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

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

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

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

10.1 CountDownLatch

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

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

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

10.1.2 原理

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

10.2 回環(huán)屏障 CyclicBarrier

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

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

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

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

10.3 信號(hào)量 Semaphore

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

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

11. 并發(fā)編程實(shí)踐--一些注意事項(xiàng)

11.1 ArrayBlockingQueue

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

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

11.2 ConcurrentHashMap

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

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

11.3 SimpleDateFormat

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

那如何解決呢?

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

11.4 Timer

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

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

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

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

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

11.6 有關(guān) FutureTask

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

11.7 有關(guān)ThreadLocal

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

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

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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