Java多線程

1. 多線程

image.png
  1. 新建狀態(tài): 一個新產生的線程從新狀態(tài)開始了它的生命周期。它保持這個狀態(tài)直到程序 start 這個線程吟温。
  2. 運行狀態(tài):當一個新狀態(tài)的線程被 start 以后,線程就變成可運行狀態(tài)突颊,一個線程在此狀態(tài)下被認為是開始執(zhí)行其任務
  3. 就緒狀態(tài):當一個線程等待另外一個線程執(zhí)行一個任務的時候鲁豪,該線程就進入就緒狀態(tài)。當另一個線程給就緒狀態(tài)的線程發(fā)送信號時律秃,該線程才重新切換到運行狀態(tài)爬橡。
  4. 休眠狀態(tài): 由于一個線程的時間片用完了,該線程從運行狀態(tài)進入休眠狀態(tài)友绝。當時間間隔到期或者等待的時間發(fā)生了堤尾,該狀態(tài)的線程切換到運行狀態(tài)。
  5. 終止狀態(tài): 一個運行狀態(tài)的線程完成任務或者其他終止條件發(fā)生迁客,該線程就切換到終止狀態(tài)郭宝。

2. 僵死進程

計算機的計算模型大部分是基于空間和時間來考慮的。僵死進程唯一占用的空間是pid空間掷漱,這個空間如果不能合理的應用就會造成浪費粘室,之所以保留這個空間,是為了讓父進程感知子進程已經終止這個行為卜范。時間方面衔统,這個感知過程是一個異步的過程。

3. 創(chuàng)建線程的方式

繼承 Thread 類
實現(xiàn) Runnable 接口
使用 Executor 框架

法一:繼承Thread類

1.1定義一個類繼承Thread

1.2重寫run方法

1.3創(chuàng)建對象

1.4調用start方法開啟線程

線程對象調用run()方法和start()方法區(qū)別?

調用run方法不開啟線程海雪,僅是對象調用方法锦爵。

調用start方法開啟線程,并讓jvm調用run方法在開啟的線程中執(zhí)行奥裸。

run()方法用來執(zhí)行線程體中具體的內容

start()方法用來啟動線程對象险掀,使其進入就緒狀態(tài)

法二:實現(xiàn)Runnable接口

2.1定義一個類實現(xiàn)Runnable接口。

2.2覆蓋run()方法湾宙。

2.3創(chuàng)建子類對象樟氢。

2.4創(chuàng)建Thread類對象,將實現(xiàn)Runnable接口的子類對象作為參數(shù)傳遞給Thread類對象的構造函數(shù)侠鳄。

2.5調用start方法開啟線程埠啃。

Runnable優(yōu)點:避免了繼承Thread類的單繼承局限性,更加符合面向對象

3. 線程安全類

何為線程安全的類

在線程安全性的定義中伟恶,最核心的概念就是 正確性碴开。當多個線程訪問某個類時,不管運行時環(huán)境采用何種調度方式或者這些線程將如何交替執(zhí)行博秫,并且在主調代碼中不需要任何額外的同步或協(xié)同鹃骂,這個類都能表現(xiàn)出正確的行為台盯,那么這個類就是線程安全的罢绽。

線程安全類

在集合框架中,有些類是線程安全的静盅,這些都是jdk1.1中的出現(xiàn)的良价。在jdk1.2之后,就出現(xiàn)許許多多非線程安全的類蒿叠。 下面是這些線程安全的同步的類:

vector:就比arraylist多了個同步化機制(線程安全)明垢,因為效率較低,現(xiàn)在已經不太建議使用市咽。在web應用中痊银,特別是前臺頁面,往往效率(頁面響應速度)是優(yōu)先考慮的施绎。

statck:堆棧類罢浇,先進后出

hashtable:就比hashmap多了個線程安全

enumeration:枚舉痹兜,相當于迭代器

除了這些之外,其他的都是非線程安全的類和接口。

線程安全的類其方法是同步的顺饮,每次只能一個訪問。是重量級對象薄榛,效率較低岔激。

4. 如何確保線程安全

在Java中可以有很多方法來保證線程安全,諸如:

通過加鎖(Lock/Synchronized)保證對臨界資源的同步互斥訪問遇八;

使用volatile關鍵字矛绘,輕量級同步機制,但不保證原子性刃永;

使用不變類 和 線程安全類(原子類货矮,并發(fā)容器,同步容器等)揽碘。

5. 什么是死鎖

兩個線程或兩個以上線程都在等待對方執(zhí)行完畢才能繼續(xù)往下執(zhí)行的時候就發(fā)生了死鎖次屠。結果就是這些線程都陷入了無限的等待中

6. wait()與 sleep()的區(qū)別

sleep()來自 Thread 類,wait()來自 Object 類雳刺;

調用 sleep()方法劫灶,線程不會釋放對象鎖。而調用 wait 方法線程會釋放對象鎖掖桦;

sleep()睡眠后不出讓系統(tǒng)資源本昏,wait 讓其他線程可以占用 CPU;

sleep(milliseconds)需要指定一個睡眠時間枪汪,時間一到會自動喚醒涌穆。而 wait()需要配合 notify()

或者 notifyAll()使用怔昨。

7. 為什么wait(), notify()和notifyAll()必須在同步方法或者同步塊中被調用

wait/notify機制是依賴于Java中Synchronized同步機制的,其目的在于確保等待線程從Wait()返回時能夠感知通知線程對共享變量所作出的修改宿稀。如果不在同步范圍內使用趁舀,就會拋出java.lang.IllegalMonitorStateException的異常。

8. 什么是 ThreadLocal祝沸?ThreadLocal 和 Synchonized 的區(qū)別矮烹?

線程局部變量。是局限于線程內部的變量罩锐,屬于線程自身所有奉狈,不在多個線程間共享。Java提供 ThreadLocal 類來支持線程局部變量涩惑,是一種實現(xiàn)線程安全的方式仁期。

synchronized 是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問竭恬。而 ThreadLocal 為每一個線程都提供了變量的副本跛蛋,使得每個線程在某一時間訪問到的并不是同一個對象,這樣就隔離了多個線程對數(shù)據的數(shù)據共享萍聊。

9. ThreadLocal中的內存泄露問題(OOM):

如果ThreadLocal被設置為null后问芬,并且沒有任何強引用指向它,根據垃圾回收的可達性分析算法寿桨,ThreadLocal將被回收此衅。這樣的話,ThreadLocalMap中就會含有key為null的Entry亭螟,而且ThreadLocalMap是在Thread中的挡鞍,只要線程遲遲不結束,這些無法訪問到的value就會形成內存泄露预烙。為了解決這個問題墨微,ThreadLocalMap中的getEntry()、set()和remove()函數(shù)都會清理key為null的Entry扁掸,以下面的getEntry()函數(shù)為例翘县。

要注意的是ThreadLocalMap的key是一個弱引用。在這里我們分析一下強引用key和弱引用key的差別

強引用key:ThreadLocal被設置為null谴分,由于ThreadLocalMap持有ThreadLocal的強引用锈麸,如果不手動刪除,那么ThreadLocal將不會回收牺蹄,產生內存泄漏忘伞。

弱引用key:ThreadLocal被設置為null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手動刪除氓奈,ThreadLocal仍會被回收翘魄,ThreadLocalMap在之后調用set()、getEntry()和remove()函數(shù)時會清除所有key為null的Entry舀奶。

ThreadLocalMap僅僅含有這些被動措施來補救內存泄露問題暑竟,如果在之后沒有調用ThreadLocalMap的set()、getEntry()和remove()函數(shù)的話伪节,那么仍然會存在內存泄漏問題光羞。在使用線程池的情況下,如果不及時進行清理怀大,內存泄漏問題事小,甚至還會產生程序邏輯上的問題呀闻。所以化借,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣捡多,在每次使用完ThreadLocal后都要調用remove()來清理無用的Entry蓖康。

10. 多線程常見問題

上下文切換

多線程并不一定是要在多核處理器才支持的,就算是單核也是可以支持多線程的垒手。 CPU 通過給每個線程分配一定的時間片蒜焊,由于時間非常短通常是幾十毫秒,所以 CPU 可以不停的切換線程執(zhí)行任務從而達到了多線程的效果科贬。

但是由于在線程切換的時候需要保存本次執(zhí)行的信息,在該線程被 CPU 剝奪時間片后又再次運行恢復上次所保存的信息的過程就稱為上下文切換泳梆。

上下文切換是非常耗效率的。

通常有以下解決方案:

  • 采用無鎖編程榜掌,比如將數(shù)據按照 Hash(id) 進行取模分段优妙,每個線程處理各自分段的數(shù)據,從而避免使用鎖憎账。

  • 采用 CAS(compare and swap) 算法套硼,如 Atomic 包就是采用 CAS 算法。

  • 合理的創(chuàng)建線程胞皱,避免創(chuàng)建了一些線程但其中大部分都是處于 waiting 狀態(tài)邪意,因為每當從 waiting 狀態(tài)切換到 running 狀態(tài)都是一次上下文切換。

死鎖

死鎖的場景一般是:線程 A 和線程 B 都在互相等待對方釋放鎖反砌,或者是其中某個線程在釋放鎖的時候出現(xiàn)異常如死循環(huán)之類的雾鬼。這時就會導致系統(tǒng)不可用。

常用的解決方案如下:

  • 盡量一個線程只獲取一個鎖于颖。

  • 一個線程只占用一個資源呆贿。

  • 嘗試使用定時鎖,至少能保證鎖最終會被釋放。

資源限制

當在帶寬有限的情況下一個線程下載某個資源需要 1M/S,當開 10 個線程時速度并不會乘 10 倍做入,反而還會增加時間冒晰,畢竟上下文切換比較耗時。如果是受限于資源的話可以采用集群來處理任務竟块,不同的機器來處理不同的數(shù)據壶运,就類似于開始提到的無鎖編程

11. synchronized 關鍵字原理

1)synchronized 關鍵字是解決并發(fā)問題常用解決方案,有以下三種使用方式:

  • 同步普通方法浪秘,鎖的是當前對象蒋情。

  • 同步靜態(tài)方法,鎖的是當前 Class 對象耸携。

  • 同步塊棵癣,鎖的是 () 中的對象。

實現(xiàn)原理: JVM 是通過進入夺衍、退出對象監(jiān)視器( Monitor )來實現(xiàn)對方法狈谊、同步塊的同步的

2)鎖優(yōu)化

synchronized 很多都稱之為重量鎖,JDK1.6 中對 synchronized 進行了各種優(yōu)化沟沙,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖河劝。

輕量鎖

當代碼進入同步塊時,如果同步對象為無鎖狀態(tài)時矛紫,當前線程會在棧幀中創(chuàng)建一個鎖記錄(Lock Record)區(qū)域赎瞎,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word 更新為指向鎖記錄的指針颊咬。

如果更新成功务甥,當前線程就獲得了鎖。

如果更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄贪染。

如果是則說明當前線程擁有鎖對象的鎖缓呛,可以直接進入同步塊。

不是則說明有其他線程搶占了鎖杭隙,如果存在多個線程同時競爭一把鎖哟绊,輕量鎖就會膨脹為重量鎖。

解鎖

輕量鎖的解鎖過程也是利用 CAS 來實現(xiàn)的痰憎,會嘗試鎖記錄替換回鎖對象的 Mark Word 票髓。如果替換成功則說明整個同步操作完成,失敗則說明有其他線程嘗試獲取鎖铣耘,這時就會喚醒被掛起的線程(此時已經膨脹為重量鎖)

輕量鎖能提升性能的原因是:

認為大多數(shù)鎖在整個同步周期都不存在競爭洽沟,所以使用 CAS 比使用互斥開銷更少。但如果鎖競爭激烈蜗细,輕量鎖就不但有互斥的開銷裆操,還有 CAS 的開銷怒详,甚至比重量鎖更慢。

偏向鎖

為了進一步的降低獲取鎖的代價踪区,JDK1.6 之后還引入了偏向鎖昆烁。

偏向鎖的特征是:鎖不存在多線程競爭,并且應由一個線程多次獲得鎖缎岗。

當線程訪問同步塊時静尼,會使用 CAS 將線程 ID 更新到鎖對象的 Mark Word 中,如果更新成功則獲得偏向鎖传泊,并且之后每次進入這個對象鎖相關的同步塊時都不需要再次獲取鎖了鼠渺。

釋放鎖

當有另外一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖眷细,釋放時會等待全局安全點(這一時刻沒有字節(jié)碼運行)拦盹,接著會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來判定將對象頭中的 Mark Word 設置為無鎖或者是輕量鎖狀態(tài)薪鹦。

偏向鎖可以提高帶有同步卻沒有競爭的程序性能掌敬,但如果程序中大多數(shù)鎖都存在競爭時,那偏向鎖就起不到太大作用池磁。可以使用 -XX:-UseBiasedLocking 來關閉偏向鎖楷兽,并默認進入輕量鎖地熄。

|

12.ReentrantLock 和Synchronized的區(qū)別

1、ReentrantLock 擁有Synchronized相同的并發(fā)性和內存語義芯杀,此外還多了 鎖投票端考,定時鎖等候和中斷鎖等候
線程A和B都要獲取對象O的鎖定,假設A獲取了對象O鎖揭厚,B將等待A釋放對O的鎖定却特,
如果使用 synchronized ,如果A不釋放筛圆,B將一直等下去裂明,不能被中斷
如果 使用ReentrantLock,如果A不釋放太援,可以使B在等待了足夠長的時間以后闽晦,中斷等待,而干別的事情
ReentrantLock獲取鎖定與三種方式:
a) lock(), 如果獲取了鎖立即返回提岔,如果別的線程持有鎖仙蛉,當前線程則一直處于休眠狀態(tài),直到獲取鎖
b) tryLock(), 如果獲取了鎖立即返回true碱蒙,如果別的線程正持有鎖荠瘪,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果獲取了鎖定立即返回true哀墓,如果別的線程正持有鎖趁餐,會等待參數(shù)給定的時間,在等待的過程中麸祷,如果獲取了鎖定澎怒,就返回true,如果等待超時阶牍,返回false喷面;
d) lockInterruptibly:如果獲取了鎖定立即返回,如果沒有獲取鎖定走孽,當前線程處于休眠狀態(tài)惧辈,直到或者鎖定,或者當前線程被別的線程中斷

2磕瓷、synchronized是在JVM層面上實現(xiàn)的盒齿,不但可以通過一些監(jiān)控工具監(jiān)控synchronized的鎖定,而且在代碼執(zhí)行時出現(xiàn)異常困食,JVM會自動釋放鎖定边翁,但是使用Lock則不行,lock是通過代碼實現(xiàn)的硕盹,要保證鎖定一定會被釋放符匾,就必須將unLock()放到finally{}中

3、在資源競爭不是很激烈的情況下瘩例,Synchronized的性能要優(yōu)于ReetrantLock啊胶,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍垛贤,但是ReetrantLock的性能能維持常態(tài)焰坪;

13.Java 多線程三大核心

1)原子性
Java 的原子性就和數(shù)據庫事務的原子性差不多,一個操作中要么全部執(zhí)行成功或者失敗聘惦。
2)可見性
現(xiàn)代計算機中某饰,由于 CPU 直接從主內存中讀取數(shù)據的效率不高,所以都會對應的 CPU 高速緩存部凑,先將主內存中的數(shù)據讀取到緩存中露乏,線程修改數(shù)據之后首先更新到緩存,之后才會更新到主內存涂邀。如果此時還沒有將數(shù)據更新到主內存其他的線程此時來讀取就是修改之前的數(shù)據瘟仿。

image.jpeg

如上圖所示。

volatile 關鍵字就是用于保證內存可見性比勉,當線程A更新了 volatile 修飾的變量時劳较,它會立即刷新到主線程驹止,并且將其余緩存中該變量的值清空,導致其余線程只能去主內存讀取最新值观蜗。

使用 volatile 關鍵詞修飾的變量每次讀取都會得到最新的數(shù)據臊恋,不管哪個線程對這個變量的修改都會立即刷新到主內存。

synchronized和加鎖也能能保證可見性墓捻,實現(xiàn)原理就是在釋放鎖之前其余線程是訪問不到這個共享變量的抖仅。但是和 volatile 相比開銷較大。

3)順序性

int a = 100 ; //1int b = 200 ; //2int c = a + b ; //3

正常情況下的執(zhí)行順序應該是 1>>2>>3砖第。但是有時 JVM 為了提高整體的效率會進行指令重排導致執(zhí)行的順序可能是 2>>1>>3撤卢。但是 JVM 也不能是什么都進行重排,是在保證最終結果和代碼順序執(zhí)行結果一致的情況下才可能進行重排梧兼。

重排在單線程中不會出現(xiàn)問題放吩,但在多線程中會出現(xiàn)數(shù)據不一致的問題。

Java 中可以使用 volatile 來保證順序性羽杰,synchronized 和 lock 也可以來保證有序性渡紫,和保證原子性的方式一樣,通過同一段時間只能一個線程訪問來實現(xiàn)的考赛。
除了通過 volatile 關鍵字顯式的保證順序之外惕澎, JVM 還通過 happen-before 原則來隱式的保證順序性。
其中有一條就是適用于 volatile 關鍵字的颜骤,針對于 volatile 關鍵字的寫操作肯定是在讀操作之前集灌,也就是說讀取的值肯定是最新的。

總結

volatile 關鍵字只能保證可見性复哆,順序性,不能保證原子性腌零。

14 .volatile關鍵字在Java中有什么作用

volatile的特殊規(guī)則保證了新值能立即同步到主內存梯找,以及每次使用前立即從主內存刷新,即保證了內存的可見性益涧,除此之外還能 禁止指令重排序锈锤。此外,synchronized關鍵字也可以保證內存可見性闲询。
  指令重排序問題在并發(fā)環(huán)境下會導致線程安全問題久免,volatile關鍵字通過禁止指令重排序來避免這一問題。而對于Synchronized關鍵字扭弧,其所控制范圍內的程序在執(zhí)行時獨占的阎姥,指令重排序問題不會對其產生任何影響,因此無論如何鸽捻,其都可以保證最終的正確性呼巴。

15. volatile關鍵字和synchronized關鍵字的區(qū)別

(1)泽腮、volatile只能作用于變量,使用范圍較小衣赶。synchronized可以用在變量诊赊、方法、類府瞄、同步代碼塊等碧磅,使用范圍比較廣。
(2)遵馆、volatile只能保證可見性和有序性鲸郊,不能保證原子性。而可見性团搞、有序性严望、子性synchronized都可以包證。
(3)逻恐、volatile不會造成線程阻塞像吻。synchronized可能會造成線程阻塞。

16. 對于鎖的一些認知

同一進程

重入鎖

使用 ReentrantLock 獲取鎖的時候會判斷當前線程是否為獲取鎖的線程复隆,如果是則將同步的狀態(tài) +1 ,釋放鎖的時候則將狀態(tài) -1拨匆。只有將同步狀態(tài)的次數(shù)置為 0 的時候才會最終釋放鎖。

讀寫鎖

使用 ReentrantReadWriteLock ,同時維護一對鎖:讀鎖和寫鎖挽拂。當寫線程訪問時則其他所有鎖都將阻塞惭每,讀線程訪問時則不會。通過讀寫鎖的分離可以很大程度的提高并發(fā)量和吞吐量亏栈。

不同進程

分布式鎖:

基于數(shù)據庫

可以創(chuàng)建一張表台腥,將其中的某個字段設置為唯一索引,當多個請求過來的時候只有新建記錄成功的請求才算獲取到鎖绒北,當使用完畢刪除這條記錄的時候即釋放鎖黎侈。

存在的問題:

數(shù)據庫單點問題,掛了怎么辦闷游?
不是重入鎖峻汉,同一進程無法在釋放鎖之前再次獲得鎖,因為數(shù)據庫中已經存在了一條記錄了脐往。
鎖是非阻塞的休吠,一旦 insert 失敗則會立即返回,并不會進入阻塞隊列只能下一次再次獲取业簿。
鎖沒有失效時間瘤礁,如果那個進程解鎖失敗那就沒有請求可以再次獲取鎖了。
解決方案:
數(shù)據庫切換為主從辖源,不存在單點蔚携。
在表中加入一個同步狀態(tài)字段希太,每次獲取鎖的是加 1 ,釋放鎖的時候-1酝蜒,當狀態(tài)為 0 的時候就刪除這條記錄誊辉,即釋放鎖。
非阻塞的情況可以用 while 循環(huán)來實現(xiàn)亡脑,循環(huán)的時候記錄時間堕澄,達到 X 秒記為超時,break霉咨。
可以開啟一個定時任務每隔一段時間掃描找出多少 X 秒都沒有被刪除的記錄蛙紫,主動刪除這條記錄。
基于 Redis
使用 setNX(key) setEX(timeout) 命令途戒,只有在該 key 不存在的時候創(chuàng)建這個 key坑傅,就相當于獲取了鎖。由于有超時時間喷斋,所以過了規(guī)定時間會自動刪除唁毒,這樣也可以避免死鎖。

17. 線程池

簡單來說使用線程池有以下幾個目的:

  • 線程是稀缺資源星爪,不能頻繁的創(chuàng)建浆西。

  • 解耦作用;線程的創(chuàng)建于執(zhí)行完全分開顽腾,方便維護近零。

  • 應當將其放入一個池子中,可以給其他任務進行復用抄肖。

18. 線程池原理

談到線程池就會想到池化技術久信,其中最核心的思想就是把寶貴的資源放到一個池子中;每次使用都從里面獲取漓摩,用完之后又放回池子供其他人使用入篮,有點吃大鍋飯的意思。

那在 Java 中又是如何實現(xiàn)的呢幌甘?

在 JDK 1.5 之后推出了相關的 api,常見的創(chuàng)建線程池方式有以下幾種:

  • Executors.newCachedThreadPool():無限線程池痊项。

  • Executors.newFixedThreadPool(nThreads):創(chuàng)建固定大小的線程池锅风。

  • Executors.newSingleThreadExecutor():創(chuàng)建單個線程的線程池。

這幾個核心參數(shù)的作用:

  • corePoolSize 為線程池的基本大小鞍泉。

  • maximumPoolSize 為線程池最大線程大小皱埠。

  • keepAliveTime 和 unit 則是線程空閑后的存活時間。

  • workQueue 用于存放任務的阻塞隊列咖驮。

  • handler 當隊列和最大線程池都滿了之后的飽和策略边器。

了解了這幾個參數(shù)再來看看實際的運用训枢。

這里借助《聊聊并發(fā)》的一張圖來描述這個流程:

image.jpeg

線程池的飽和策略

當阻塞隊列滿了,且沒有空閑的工作線程忘巧,如果繼續(xù)提交任務恒界,必須采取一種策略處理該任務,線程池提供了4種策略:

AbortPolicy:直接拋出異常砚嘴,默認策略十酣;

CallerRunsPolicy:用調用者所在的線程來執(zhí)行任務;

DiscardOldestPolicy:丟棄阻塞隊列中最老的任務际长,并執(zhí)行當前任務耸采;

DiscardPolicy:直接丟棄任務;

當然也可以根據應用場景實現(xiàn)RejectedExecutionHandler接口工育,自定義飽和策略虾宇,如記錄日志或持久化存儲不能處理的任務。

線程池調優(yōu)

設置最大線程數(shù)如绸,防止線程資源耗盡嘱朽;

使用有界隊列,從而增加系統(tǒng)的穩(wěn)定性和預警能力(飽和策略)竭沫;

根據任務的性質設置線程池大性锍帷:CPU密集型任務(CPU個數(shù)個線程),IO密集型任務(CPU個數(shù)兩倍的線程)蜕提,混合型任務(拆分)森书。

如何配置線程池

流程聊完了再來看看上文提到了幾個核心參數(shù)應該如何配置呢?

有一點是肯定的谎势,線程池肯定是不是越大越好凛膏。

通常我們是需要根據這批任務執(zhí)行的性質來確定的。

  • IO 密集型任務:由于線程并不是一直在運行脏榆,所以可以盡可能的多配置線程猖毫,比如 CPU 個數(shù) * 2

  • CPU 密集型任務(大量復雜的運算)應當分配較少的線程,比如 CPU 個數(shù)相當?shù)拇笮 ?/p>

當然這些都是經驗值须喂,最好的方式還是根據實際情況測試得出最佳配置吁断。

19. 優(yōu)雅的關閉線程池

有運行任務自然也有關閉任務,從上文提到的 5 個狀態(tài)就能看出如何來關閉線程池坞生。

其實無非就是兩個方法 shutdown()/shutdownNow()仔役。

但他們有著重要的區(qū)別:

  • shutdown() 執(zhí)行后停止接受新任務,會把隊列的任務執(zhí)行完畢是己。

  • shutdownNow() 也是停止接受新任務又兵,但會中斷所有的任務,將線程池狀態(tài)變?yōu)?stop。

兩個方法都會中斷線程沛厨,用戶可自行判斷是否需要響應中斷宙地。

shutdownNow() 要更簡單粗暴,可以根據實際場景選擇不同的方法逆皮。

20. springBoot使用線程池

2018 年了宅粥,SpringBoot 盛行;來看看在 SpringBoot 中應當怎么配置和使用線程池页屠。

其實也挺簡單粹胯,就是創(chuàng)建了一個線程池的 bean,在使用時直接從 Spring 中取出即可辰企。

監(jiān)控線程池

談到了 SpringBoot风纠,也可利用它 actuator 組件來做線程池的監(jiān)控。

線程怎么說都是稀缺資源牢贸,對線程池的監(jiān)控可以知道自己任務執(zhí)行的狀況竹观、效率等。

線程池的隔離

線程池看似很美好潜索,但也會帶來一些問題臭增。

如果我們很多業(yè)務都依賴于同一個線程池,當其中一個業(yè)務因為各種不可控的原因消耗了所有的線程,導致線程池全部占滿竹习。

這樣其他的業(yè)務也就不能正常運轉了誊抛,這對系統(tǒng)的打擊是巨大的。

比如我們 Tomcat 接受請求的線程池整陌,假設其中一些響應特別慢拗窃,線程資源得不到回收釋放;線程池慢慢被占滿泌辫,最壞的情況就是整個應用都不能提供服務随夸。

所以我們需要將線程池進行隔離。

通常的做法是按照業(yè)務進行劃分:

比如下單的任務用一個線程池震放,獲取數(shù)據的任務用另一個線程池宾毒。這樣即使其中一個出現(xiàn)問題把線程池耗盡,那也不會影響其他的任務運行殿遂。

hystrix隔離

這樣的需求 Hystrix 已經幫我們實現(xiàn)了诈铛。

Hystrix 是一款開源的容錯插件,具有依賴隔離墨礁、系統(tǒng)容錯降級等功能癌瘾。

下面來看看 Hystrix 簡單的應用:

首先需要定義兩個線程池,分別用于執(zhí)行訂單饵溅、處理用戶。

可以看到兩個任務分成了兩個線程池運行妇萄,他們之間互不干擾蜕企。

獲取任務任務結果支持同步阻塞和異步非阻塞方式咬荷,可自行選擇。

它的實現(xiàn)原理其實容易猜到:

利用一個 Map 來存放不同業(yè)務對應的線程池轻掩。

21. 深入理解線程通信

前言

開發(fā)中不免會遇到需要所有子線程執(zhí)行完畢通知主線程處理某些邏輯的場景幸乒。

或者是線程 A 在執(zhí)行到某個條件通知線程 B 執(zhí)行某個操作。

可以通過以下幾種方式實現(xiàn):

等待通知機制

等待通知模式是 Java 中比較經典的線程通信方式唇牧。

兩個線程通過對同一對象調用等待 wait() 和通知 notify() 方法來進行通訊罕扎。

有一些需要注意:

  • wait() 、notify()丐重、notifyAll() 調用的前提都是獲得了對象的鎖(也可稱為對象監(jiān)視器)腔召。

  • 調用 wait() 方法后線程會釋放鎖,進入 WAITING 狀態(tài)扮惦,該線程也會被移動到等待隊列中臀蛛。

  • 調用 notify() 方法會將等待隊列中的線程移動到同步隊列中,線程狀態(tài)也會更新為 BLOCKED

  • 從 wait() 方法返回的前提是調用 notify() 方法的線程釋放鎖崖蜜,wait() 方法的線程獲得鎖浊仆。

join()方法

22. volatitle共享內存

CounDownLatch并發(fā)工具

CountDownLatch 可以實現(xiàn) join 相同的功能,但是更加的靈活豫领。

CountDownLatch 也是基于 AQS(AbstractQueuedSynchronizer) 實現(xiàn)的抡柿,

  • 初始化一個 CountDownLatch 時告訴并發(fā)的線程,然后在每個線程處理完畢之后調用 countDown() 方法等恐。

  • 該方法會將 AQS 內置的一個 state 狀態(tài) -1 洲劣。

  • 最終在主線程調用 await() 方法,它會阻塞直到 state == 0 的時候返回鼠锈。

CyclicBarrier 并發(fā)工具
CyclicBarrier 中文名叫做屏障或者是柵欄闪檬,也可以用于線程間通信。

它可以等待 N 個線程都達到某個狀態(tài)后繼續(xù)運行的效果购笆。

  1. 首先初始化線程參與者粗悯。

  2. 調用 await() 將會在所有參與者線程都調用之前等待。

  3. 直到所有參與者都調用了 await() 后同欠,所有線程從 await() 返回繼續(xù)后續(xù)邏輯样傍。

可以看出由于其中一個線程休眠了五秒,所有其余所有的線程都得等待這個線程調用 await() 铺遂。

該工具可以實現(xiàn) CountDownLatch 同樣的功能衫哥,但是要更加靈活。甚至可以調用 reset() 方法重置 CyclicBarrier (需要自行捕獲 BrokenBarrierException 處理) 然后重新執(zhí)行襟锐。

線程響應中斷
可以采用中斷線程的方式來通信撤逢,調用了 thread.interrupt() 方法其實就是將 thread 中的一個標志屬性置為了 true。

并不是說調用了該方法就可以中斷線程,如果不對這個標志進行響應其實是沒有什么作用(這里對這個標志進行了判斷)蚊荣。

但是如果拋出了 InterruptedException 異常初狰,該標志就會被 JVM 重置為 false。

線程池 awaitTermination() 方法

使用這個 awaitTermination() 方法的前提需要關閉線程池互例,如調用了 shutdown() 方法奢入。

調用了 shutdown() 之后線程池會停止接受新任務,并且會平滑的關閉線程池中現(xiàn)有的任務媳叨。

管道通信

Java 雖說是基于內存通信的腥光,但也可以使用管道通信簇秒。

需要注意的是勋颖,輸入流和輸出流需要首先建立連接。這樣線程 B 就可以收到線程 A 發(fā)出的消息了缭保。

實際開發(fā)中可以靈活根據需求選擇最適合的線程通信方式扩然。

23. CAS(無鎖算法)

CAS(Compare And Swap) 無鎖算法: CAS是樂觀鎖技術艘儒,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值夫偶,而其它線程都失敗界睁,失敗的線程并不會被掛起,而是被告知這次競爭中失敗兵拢,并可以再次嘗試翻斟。CAS有3個操作數(shù),內存值V说铃,舊的預期值A访惜,要修改的新值B。當且僅當預期值A和內存值V相同時腻扇,將內存值V修改為B债热,否則什么都不做

CAS : CAS自旋volatile變量,是一種很經典的用法幼苛。
  CAS窒篱,Compare and Swap即比較并交換,設計并發(fā)算法時常用到的一種技術舶沿。CAS有3個操作數(shù)墙杯,內存值V,舊的預期值A括荡,新值B高镐。當且僅當預期值A和內存值V相同時,將內存值V修改為B畸冲,否則什么都不做嫉髓。CAS是通過unsafe類的compareAndSwap (JNI, Java Native Interface) 方法實現(xiàn)的观腊,該方法包括四個參數(shù):第一個參數(shù)是要修改的對象,第二個參數(shù)是對象中要修改變量的偏移量算行,第三個參數(shù)是修改之前的值恕沫,第四個參數(shù)是預想修改后的值。

CAS雖然很高效的解決原子操作纱意,但是CAS仍然存在三大問題:ABA問題、循環(huán)時間長開銷大和只能保證一個共享變量的原子操作鲸阔。

ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化偷霉,如果沒有發(fā)生變化則更新,但是如果一個值原來是A褐筛,變成了B类少,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化渔扎,但是實際上卻變化了硫狞。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號晃痴,每次變量更新的時候把版本號加一残吩,那么A-B-A 就會變成1A-2B-3A。

不適用于競爭激烈的情形中:并發(fā)越高倘核,失敗的次數(shù)會越多泣侮,CAS如果長時間不成功,會極大的增加CPU的開銷紧唱。因此CAS不適合競爭十分頻繁的場景活尊。

只能保證一個共享變量的原子操作:當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作漏益,但是對多個共享變量操作時蛹锰,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖绰疤,或者有一個取巧的辦法铜犬,就是把多個共享變量合并成一個共享變量來操作。比如有兩個共享變量i=2,j=a峦睡,合并一下ij=2a翎苫,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性榨了,因此可以把多個變量放在一個對象里來進行CAS操作煎谍。

AQS : 隊列同步器

隊列同步器(AbstractQueuedSynchronizer)是用來構建鎖和其他同步組件的基礎框架,技術是 CAS自旋Volatile變量:它使用了一個Volatile成員變量表示同步狀態(tài)龙屉,通過CAS修改該變量的值呐粘,修改成功的線程表示獲取到該鎖满俗;若沒有修改成功,或者發(fā)現(xiàn)狀態(tài)state已經是加鎖狀態(tài)作岖,則通過一個Waiter對象封裝線程唆垃,添加到等待隊列中,并掛起等待被喚醒痘儡。

同步器是實現(xiàn)鎖的關鍵辕万,子類通過繼承同步器并實現(xiàn)它的抽象方法來管理同步狀態(tài),利用同步器實現(xiàn)鎖的語義沉删。特別地渐尿,鎖是面向鎖使用者的,它定義了使用者與鎖交互的接口矾瑰,隱藏了實現(xiàn)細節(jié)砖茸;同步器面向的是鎖的實現(xiàn)者,它簡化了鎖的實現(xiàn)方式殴穴,屏蔽了同步狀態(tài)管理凉夯、線程排隊、等待與喚醒等底層操作采幌。鎖和同步器很好地隔離了鎖的使用者與鎖的實現(xiàn)者所需關注的領域劲够。

一般來說,自定義同步器要么是獨占方式植榕,要么是共享方式再沧,他們也只需實現(xiàn)tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可尊残。但AQS也支持自定義同步器同時實現(xiàn)獨占和共享兩種方式炒瘸,如ReentrantReadWriteLock。

同步器的設計是基于 模板方法模式 的寝衫,也就是說顷扩,使用者需要繼承同步器并重寫指定的方法,隨后將同步器組合在自定義同步組件的實現(xiàn)中慰毅,并調用同步器提供的模板方法隘截,而這些模板方法將會調用使用者重寫的方法。

AQS維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)汹胃。這里volatile是核心關鍵詞婶芭,具體volatile的語義,在此不述着饥。state的訪問方式有三種:getState()犀农、setState()以及compareAndSetState()。

AQS定義了兩種資源共享方式:Exclusive(獨占宰掉,只有一個線程能執(zhí)行呵哨,如ReentrantLock)和Share(共享赁濒,多個線程可同時執(zhí)行,如Semaphore/CountDownLatch)孟害。不同的自定義同步器爭用共享資源的方式也不同拒炎。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等)挨务,AQS已經在頂層實現(xiàn)好了击你。自定義同步器實現(xiàn)時主要實現(xiàn)以下幾種方法:

isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現(xiàn)它谎柄;

tryAcquire(int):獨占方式果漾。嘗試獲取資源,成功則返回true谷誓,失敗則返回false;

tryRelease(int):獨占方式吨凑。嘗試釋放資源捍歪,成功則返回true,失敗則返回false鸵钝;

tryAcquireShared(int):共享方式糙臼。嘗試獲取資源。負數(shù)表示失敹魃獭变逃;0表示成功,但沒有剩余可用資源怠堪;正數(shù)表示成功揽乱,且有剩余資源;

tryReleaseShared(int):共享方式粟矿。嘗試釋放資源凰棉,成功則返回true,失敗則返回false陌粹。

以ReentrantLock為例撒犀,state初始化為0,表示未鎖定狀態(tài)掏秩。A線程lock()時或舞,會調用tryAcquire()獨占該鎖并將state+1。此后蒙幻,其他線程再tryAcquire()時就會失敗映凳,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖杆煞。當然魏宽,釋放鎖之前腐泻,A線程自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念队询。但要注意派桩,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態(tài)的蚌斩。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末铆惑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子送膳,更是在濱河造成了極大的恐慌员魏,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叠聋,死亡現(xiàn)場離奇詭異撕阎,居然都是意外死亡,警方通過查閱死者的電腦和手機碌补,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門虏束,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人厦章,你說我怎么就攤上這事镇匀。” “怎么了袜啃?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵汗侵,是天一觀的道長。 經常有香客問我群发,道長晰韵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任熟妓,我火速辦了婚禮宫屠,結果婚禮上,老公的妹妹穿的比我還像新娘滑蚯。我一直安慰自己浪蹂,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布告材。 她就那樣靜靜地躺著坤次,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斥赋。 梳的紋絲不亂的頭發(fā)上缰猴,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音疤剑,去河邊找鬼滑绒。 笑死闷堡,一個胖子當著我的面吹牛,可吹牛的內容都是我干的疑故。 我是一名探鬼主播杠览,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼纵势!你這毒婦竟也來了踱阿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钦铁,失蹤者是張志新(化名)和其女友劉穎软舌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牛曹,經...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡佛点,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了黎比。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恋脚。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖焰手,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情怀喉,我是刑警寧澤书妻,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站躬拢,受9級特大地震影響躲履,放射性物質發(fā)生泄漏。R本人自食惡果不足惜聊闯,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一工猜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菱蔬,春花似錦篷帅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蚪腐,卻和暖如春箭昵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背回季。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工家制, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留正林,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓颤殴,卻偏偏與公主長得像觅廓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诅病,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355