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

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

1.1 什么是線程

線程是進程中的一個實體旅急,線程本身是不會獨立存在的趋惨。進程是代碼在數(shù)據集合上的一次運行活動氢伟,是系統(tǒng)進行資源分配和調度的基本單位员帮,線程則是進程的一個執(zhí)行路徑或粮,一個進程中至少有一個線程,進程中的多個線程共享進程的資源

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

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

  • 程序計數(shù)器是一塊內存區(qū)域,用來記錄線程當前要執(zhí)行的指令地址
  • 需要注意的是型檀,如果執(zhí)行的是 native 方法冗尤,那么 pc 計數(shù)器記錄的是 undefined 地址,只有執(zhí)行的是 Java 代碼時 pc 計數(shù)器記錄的才是下一條指令的地址
  • 每個線程都有自己的棧資源胀溺,用于存儲該線程的局部變量裂七,這些局部變量是該線程私有的,其他線程是訪問不了的月幌,除此之外棧還用來存放線程的調用棧幀
  • 堆是一個進程中最大的一塊內存碍讯,堆是被進程中的所有線程共享的,是進程創(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ù)進行傳遞始藕,而如果使用 Runnable 方式,則只能使用主線程里面被聲明為 final 的變量氮趋。不好的地方是 Java 不支持多繼承伍派,而如果繼承了 Thread 類,那么子類不能再繼承其他類剩胁,而 Runnable 則沒有這個限制诉植。前兩種方式都沒辦法拿到任務執(zhí)行的返回結果,但是 FutureTask 方式可以昵观。

1.3 線程通知與等待

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

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

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

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

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

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

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

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

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

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

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

線程 A 調用線程 B 的 join 方法后會被阻塞介粘,當其他線程調用了線程 A 的 interrupt() 方法中斷了線程 A 時殖氏,線程 A 會拋出 InterruptedException 異常而返回

1.5 讓線程睡眠的 sleep 方法

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

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

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

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

1.7 線程中斷

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

  • void interrupt() : 中斷線程鳄厌,當線程 A 運行時荞胡,線程 B 可以調用線程 A 的 interrupt() 方法來設置線程 A 的中斷標志為 true 并立即返回。設置標志僅僅是設置標志了嚎,線程 A 實際并沒有被中斷泪漂,它會繼續(xù)往下執(zhí)行。
  • boolean isInterrupted() : 檢測當前線程是否被中斷歪泳,如果是返回 true萝勤,否則返回 false
  • boolean interrupted() : 檢測當前線程是否被中斷,如果是返回 true呐伞,否則返回 false敌卓,如果該方法發(fā)現(xiàn)當前線程被中斷,則會清除中斷標志伶氢,并且該方法是 static 方法趟径,可以通過 Thread 類直接調用瘪吏。

1.8 線程上下文切換

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

1.9 線程死鎖

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

死鎖的產生必須具備以下四個條件:

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

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

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

1.10 守護線程與用戶線程

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

守護線程和用戶線程區(qū)別之一是當最后一個非守護線程結束時辣恋,JVM 會正常退出第煮,而不管當前是否有守護線程,也就是說守護線程是否結束并不影響 JVM 的退出抑党。言外之意,只要有一個用戶線程還沒結束撵摆,正常情況下 JVM 就不會退出

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

總的來說,如果希望在主線程結束后 JVM 進程馬上結束特铝,那么在創(chuàng)建線程時可以將其設置為守護線程暑中,如果希望在主線程結束后子線程繼續(xù)工作壹瘟,等子線程結束后再讓 JVM 進程結束,那么就將子線程設置為用戶線程

1.11 ThreadLocal

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

ThreadLocal 是一個 HashMap 結構线欲,其中 key 就是當前 ThreadLocal 的實例引用,value 是通過 set 方法傳遞的值汽摹。ThreadLocal 變量在父線程中被設置值后李丰,在子線程中是獲取不到的。

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

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

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

2.2 Java 中的線程安全問題

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

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

2.3 Java 中共享變量的內存可見性問題

當一個線程操作共享變量時痹筛,它首先從主內存復制共享變量到自己的工作內存,然后對工作內存里的變量進行處理廓鞠,處理完后將變量值更新到主內存

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

2.4 synchronized

2.4.1 synchronized 關鍵字介紹

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

內置鎖是排它鎖杆麸,也就是當一個線程獲取這個鎖后,其他線程必須等待該線程釋放鎖后才能獲取該鎖浪感。

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

2.4.2 synchronized 的內存語義

進入 synchronized 塊的內存語義是把在 synchronized 塊內使用到的變量從線程的工作內存中清除讹开,這樣在 synchronized 塊內使用到該變量時就不會從線程的工作內存中獲取盅视,而是直接從主內存中獲取。退出 synchronized 塊的內存語義是把在 synchronized 塊內對共享變量的修改刷新到主內存旦万。

除了可以解決共享變量內存可見性問題外闹击,synchronized 經常被用來實現(xiàn)原子性操作。另外請注意成艘,synchronized 關鍵字會引起線程上下文切換并帶來線程調度開銷赏半。

2.5 volatile

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

2.6 Java 中的原子性操作

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

線程安全性:即內存可見性和原子性

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ù)辅鲸,分別為:對象內存位置、對象中的變量的偏移量腹殿、變量預期值和新的值独悴。其操作含義是,如果對象 obj 中內存偏移量為 valueOffset 的變量值為 expect 锣尉,則使用新的值 update 替換舊的值 expect刻炒。這是處理器提供的一個原子性指令。

ABA 問題
CAS 操作有個經典的 ABA 問題自沧。

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

2.8 Unsafe 類

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

2.9 Java 指令重排序

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

重排序在多線程下會導致非預期的程序執(zhí)行結果,而使用 volatile 修飾變量就可以避免重排序和內存可見性問題据悔。

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

2.10 偽共享

2.10.1 什么是偽共享

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

在 Cache 內部是按行存儲的骇径,其中每一行稱為一個 Cache 行躯肌。Cache 行是 Cache 與主內存進行數(shù)據交換的單位。

由于存放到 Cache 行的是內存塊而不是單個變量既峡,所以可能會把多個變量存放到一個 Cache 行中羡榴。當多個線程同時修改一個緩存行里面的多個變量時,由于同時只能有一個線程操作緩存行运敢,所以相比將每一個變量放到一個緩存行校仑,性能會有所下降,這就是偽共享传惠。

2.10.2 如何避免偽共享

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

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

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

2.11 鎖的概述

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

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

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

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

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

  • 非公平鎖:ReentrantLock pairLock = new ReentrantLock(false) 渺氧,默認是非公平鎖

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

2.11.3 獨占鎖與共享鎖

根據鎖只能被單個線程持有還是能被多個線程共同持有蹬屹,鎖可以分為 獨占鎖 和 共享鎖 侣背。

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

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

可重入鎖的原理是在鎖內部維護了一個線程標示拒啰,用來標示該鎖目前被哪個線程占用,然后關聯(lián)一個計數(shù)器蒂胞,當計數(shù)器值為 0 時說明該鎖沒有被任何線程占用图呢,當一個線程獲取了該鎖,計數(shù)器值會變?yōu)?1骗随,這時其他線程再來獲取鎖時會發(fā)現(xiàn)鎖的所有者不是自己而被阻塞掛起。但是當獲取了該鎖的線程再次獲取鎖時發(fā)現(xiàn)鎖擁有者是自己赴叹,計數(shù)器值就 + 1鸿染,當釋放鎖后,計數(shù)器值 - 1乞巧。當計數(shù)器值為 0 時涨椒,鎖里面的線程標示被重置為 null ,這時候被阻塞的線程會被喚醒來競爭獲取該鎖绽媒。

2.11.5 自旋鎖

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

自旋鎖則是旁蔼,當前線程在獲取鎖時,如果發(fā)現(xiàn)鎖已經被其他線程占有疙教,它不馬上阻塞自己棺聊,在不放棄 CPU 使用權的情況下,多次嘗試獲日晡健(默認次數(shù)是 10限佩,可以使用 -XX:PreBlockSpinsh 參數(shù)設置該值),很有可能在后面幾次嘗試中其他線程已經釋放了鎖裸弦。如果嘗試指定的次數(shù)后仍沒有獲取到鎖則當前線程才會被阻塞掛起祟同。

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

3. ThreadLocalRandom

3.1 Random 類及其局限性

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

3.2 ThreadLocalRandom

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

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

4. JUC 中的原子操作類

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

4.1 AtomicLong

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

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

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

4.2 LongAdder

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

另外掀序,多個線程在爭奪同一個 Cell 原子變量時如果失敗了,它并不是在當前 Cell 變量上一直自旋 CAS 重試惭婿,而是嘗試在其他 Cell 的變量上進行 CAS 嘗試不恭,這個改變增加了當前線程重試 CAS 成功的可能性叶雹。

最后,在獲取 LongAdder 當前值時换吧,是把所有 Cell 變量的 value 值累加后再加上 base 返回的折晦。

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

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

5. CopyOnWriteArrayList

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

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

6. JUC中鎖原理

6.1 LockSupport

  • LockSupport 是個工具類嬉荆,它的主要作用是掛起和喚醒線程归敬,該工具類是創(chuàng)建鎖和其他同步類的基礎。
  • LockSupport 類與每個使用它的線程都會關聯(lián)一個許可證鄙早,在默認情況下調用 LockSupport 類的方法的線程是不持有許可證的汪茧。LockSupport 是使用 Unsafe 類實現(xiàn)的。
6.1.1 void park()
  • 如果調用 park 方法的線程已經拿到了與 LockSupport 關聯(lián)的許可證限番,則調用 LockSupport.park() 時會馬上返回舱污,否則調用線程會被禁止參與線程的調度,也就是會被阻塞掛起弥虐。
6.1.2 void unpark()
  • 當一個線程調用 unpark 時扩灯,如果參數(shù) thread 線程沒有持有 thread 與 LockSupport 類關聯(lián)的許可證媚赖,則讓 thread 線程持有。
  • 如果 thread 之前因調用 park() 而被掛起驴剔,則調用 unpark() 后省古,該線程會被喚醒。
  • 如果 thread 之前沒有調用 park()丧失,則調用 unpark 方法后豺妓,再調用 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)同步器的基礎組件,并發(fā)包中鎖的底層就是使用 AQS 實現(xiàn)
  • AQS 是一個 FIFO 的雙向隊列描验,其內部通過節(jié)點 head 和 tail 記錄隊首和隊尾元素白嘁,隊列元素的類型為 Node。其中 Node 中的 thread 變量用來存放進入 AQS 隊列里的線程
  • 在 AQS 中維持了一個單一的狀態(tài)信息 state膘流,可以通過 getState絮缅、setState、compareAndSetState 函數(shù)修改其值呼股。
  • AQS 有個內部類 ConditionObject耕魄,用來結合鎖實現(xiàn)線程同步。
  • 對于 AQS 來說彭谁,線程同步的關鍵是對狀態(tài)值 state 進行操作吸奴。

6.2.1 條件變量的支持

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

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

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

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

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

  • 當多個線程同時調用 lock.lock() 方法獲取鎖時,只有一個線程獲取到了鎖燥滑,其他線程會被轉換為 Node 節(jié)點插入到 lock 鎖對應的 AQS 阻塞隊列里面渐北,并做自旋 CAS 嘗試獲取鎖。
  • 如果獲取到鎖的線程又調用了對應的條件變量的 await() 方法铭拧,則該線程會釋放獲取到的鎖赃蛛,并被轉換為 Node 節(jié)點插入到條件變量對應的條件隊列里面恃锉。
  • 這時候因為調用 lock.lock() 方法被阻塞到 AQS 隊列里面的一個線程會獲取到被釋放的鎖,如果該線程也調用了條件變量的 await() 方法則該線程也會被放入條件變量的條件隊列里面呕臂。
  • 當另外一個線程調用條件變量的 signal() 或者 signalAll() 方法時破托,會把條件隊列里面的一個或者全部 Node 節(jié)點移動到 AQS 的阻塞隊列里面,等待時機獲取鎖歧蒋。

也就是說土砂,一個鎖對應一個 AQS 阻塞隊列,對應多個條件變量谜洽,每個條件變量有自己的一個條件隊列萝映。

6.3 獨占鎖 ReentrantLock

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

6.3.1 獲取鎖

void lock()

  • 調用該方法時,如果鎖當前沒有被其他線程占用并且當前線程之前沒有獲取過該鎖实束,則當前線程會獲取到該鎖奥秆,然后設置當前鎖的擁有者為當前線程,并設置 AQS 的狀態(tài)值 state 為 1咸灿,然后直接返回吭练。如果當前線程之前已經獲取過該鎖,則這次只是簡單的把 AQS 的狀態(tài)值加 1 后返回析显。如果該鎖已經被其他線程持有,則調用該方法的線程會被放入 AQS 隊列后阻塞掛起签赃。

當然還有其他的獲取鎖的方法

  • void lockInterruptibly():對中斷進行響應
  • boolean tryLock():嘗試獲取鎖谷异,如果當前該鎖沒有被其他線程持有,則當前線程獲取該鎖并返回 true锦聊,否則返回 false歹嘹。注意,該方法不會引起當前線程阻塞孔庭。

6.3.2 釋放鎖

void unlock()

  • 嘗試釋放鎖尺上,如果當前線程持有該鎖,則調用該方法會讓線程對該線程持有的 AQS 狀態(tài)值減 1圆到,如果減去 1 后當前狀態(tài)值為 0 怎抛,則當前線程會釋放該鎖,否則僅僅減 1 而已芽淡。如果當前線程沒有持有該鎖而調用了該方法則會拋出 IllegalMonitorStateException 異常马绝。

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

6.4 讀寫鎖 ReentrantReadWriteLock

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

6.5 StampedLock

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

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

  • 寫鎖 writeLock:獨占鎖鸠姨,不可重入
  • 悲觀讀鎖 readLock:共享鎖铜秆,不可重入
  • 樂觀讀鎖 tryOptimisticRead:只是使用位操作進行檢驗,不涉及 CAS 操作讶迁,所以效率會高很多

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

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

7.1 ConcurrentLinkedQueue

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


7.2 LinkedBlockingQueue

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


7.3 ArrayBlockingQueue

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

ArrayBlockingQueue 的內部有一個數(shù)組 items 娘汞,用來存放隊列元素歹茶,putIndex 變量表示入隊元素下標,takeIndex 是出隊下標你弦,count 統(tǒng)計隊列元素個數(shù)惊豺。另外,有個獨占鎖 lock 用來保證出禽作、入隊操作的原子性尸昧,這保證了同時只有一個線程可以進行入隊、出隊操作旷偿。另外烹俗,notEmpty、notFull 條件變量用來進行出萍程、入隊的同步衷蜓。

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


7.4 PriorityBlockingQueue

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

PriorityBlockingQueue 隊列在內部使用二叉樹堆維護元素優(yōu)先級无虚,使用數(shù)組作為元素存儲的數(shù)據結構缔赠,這個數(shù)組是可擴容的。當當前元素個數(shù) >= 最大容量時會通過 CAS 算法擴容友题,出隊時始終保證出隊的元素是堆樹的根節(jié)點嗤堰,而不是在隊列里面停留時間最長的元素。使用元素的 compareTo 方法提供默認的元素優(yōu)先級比較規(guī)則度宦,用戶可以自定義優(yōu)先級的比較規(guī)則踢匣。


7.5 DelayQueue

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


8. ThreadPoolExecutor

8.1 介紹

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

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

線程池參數(shù)

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

線程池類型

  • newFixedThreadPool:創(chuàng)建一個核心線程個數(shù)和最大線程個數(shù)都為 nThreads 的線程池,并且阻塞隊列長度為 Integer.MAX_VALUE怔匣。keepAliveTime = 0 說明只要線程個數(shù)比核心線程個數(shù)多并且當前空閑則回收握联。
  • newSingleThreadExecutor:創(chuàng)建一個核心線程個數(shù)和最大線程個數(shù)都為 1 的線程池,并且阻塞隊列長度為 Integer.MAX_VALUE每瞒。keepAliveTime = 0 說明只要線程個數(shù)比核心線程個數(shù)多并且當前空閑則回收金闽。
  • newCachedThreadPool:創(chuàng)建一個按需創(chuàng)建線程的線程池,初始線程個數(shù)為 0 剿骨,最多線程個數(shù)為 Integer.MAX_VALUE代芜,并且阻塞隊列為同步隊列。keepAliveTime = 60 說明只要當前線程在 60s 內空閑則回收浓利。這個類型的特殊之處在于挤庇,加入同步隊列的任務會被馬上執(zhí)行,同步隊列里面最多只有一個任務贷掖。

public void execute(Runnable command)

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


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

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

9. ScheduledThreadPoolExecutor

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

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

9.3.2 scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
fixed-delay 類型的任務的執(zhí)行原理為,當添加一個任務到延遲隊列后爽丹,等待 initialDelay 時間筑煮,任務就會過期,過期的任務就會被從隊列移除粤蝎,并執(zhí)行真仲。執(zhí)行完畢后,會重新設置任務的延遲時間初澎,然后再把任務放入延遲隊列秸应,循環(huán)往復。需要注意的是碑宴,如果一個任務在執(zhí)行中拋出了異常软啼,那么這個任務就結束了,但是不影響其他任務的執(zhí)行延柠。

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

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

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

10.1 CountDownLatch

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

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

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

10.1.2 原理

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

10.2 回環(huán)屏障 CyclicBarrier

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

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

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

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

10.3 信號量 Semaphore

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

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

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

11.1 ArrayBlockingQueue

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

  • put() 方法是阻塞的惨缆,也就是說如果當前隊列滿糜值,則在調用 put 方法向隊列放入一個元素時調用線程會被阻塞直到隊列有空余空間。
  • offer() 方法是非阻塞的坯墨,如果當前隊列滿寂汇,則會直接返回,也就是丟棄當前元素捣染。
  • pool() 方法是從隊列頭部獲取并移除一個元素骄瓣,如果隊列為空則返回 null ,該方法是不阻塞的
  • take() 方法是獲取當前隊列頭部元素并從隊列里面移除它耍攘。如果隊列為空則阻塞當前線程直到隊列不為空然后返回元素榕栏。

11.2 ConcurrentHashMap

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

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

11.3 SimpleDateFormat

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

那如何解決呢?

1.第一種方式:每次使用時都 new 一個 SimpleDateFormat 的實例卖氨,這樣可以保證每個實例使用自己的 Calender 實例会烙,但是每次使用都 new 一個對象负懦,并且使用后由于沒有其他引用,又需要回收柏腻,開銷會很大纸厉。
2.第二種方式:出錯的原因在于其內部實現(xiàn)中步驟不是一個原子性操作,我們可以使用 synchronized 進行同步五嫂,這意味著多個線程要競爭鎖颗品,在高并發(fā)場景下會導致系統(tǒng)響應性能下降。
3.第三種方式:使用 ThreadLocal沃缘,這樣每個線程只需要使用一個 SimpleDateFormat 實例躯枢,這相比第一種方式大大節(jié)省了對象的創(chuàng)建銷毀開銷,并且不需要使用多個線程同步槐臀。但要注意锄蹂,使用完線程變量后,要進行清理(remove())水慨,以避免內存泄漏得糜。

11.4 Timer

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

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

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

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

另外,在使用線程池的情況下當程序結束時一定要記得調用 shutdown() 關閉線程池

11.6 有關 FutureTask

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

11.7 有關ThreadLocal

在線程中使用完 ThreadLocal 變量后唯绍,要及時調用 remove() 方法以避免內存泄漏。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末枝誊,一起剝皮案震驚了整個濱河市况芒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌叶撒,老刑警劉巖绝骚,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耐版,死亡現(xiàn)場離奇詭異,居然都是意外死亡压汪,警方通過查閱死者的電腦和手機粪牲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來止剖,“玉大人虑瀑,你說我怎么就攤上這事〉涡耄” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵叽奥,是天一觀的道長扔水。 經常有香客問我,道長朝氓,這世上最難降的妖魔是什么魔市? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮赵哲,結果婚禮上待德,老公的妹妹穿的比我還像新娘。我一直安慰自己枫夺,他們只是感情好将宪,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布房资。 她就那樣靜靜地躺著瘤旨,像睡著了一般刊殉。 火紅的嫁衣襯著肌膚如雪苇本。 梳的紋絲不亂的頭發(fā)上唉俗,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天乾蓬,我揣著相機與錄音稳强,去河邊找鬼奖地。 笑死吧趣,一個胖子當著我的面吹牛法竞,可吹牛的內容都是我干的。 我是一名探鬼主播强挫,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼岔霸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了俯渤?” 一聲冷哼從身側響起秉剑,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稠诲,沒想到半個月后侦鹏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诡曙,經...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年略水,在試婚紗的時候發(fā)現(xiàn)自己被綠了价卤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡渊涝,死狀恐怖慎璧,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情跨释,我是刑警寧澤胸私,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站鳖谈,受9級特大地震影響岁疼,放射性物質發(fā)生泄漏。R本人自食惡果不足惜缆娃,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一捷绒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贯要,春花似錦暖侨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宅广,卻和暖如春扳肛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背乘碑。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工挖息, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人兽肤。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓套腹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親资铡。 傳聞我的和親對象是個殘疾皇子电禀,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內容