JAVA 多線程與鎖
線程與線程池
線程安全可能出現(xiàn)的場景
- 共享變量資源多線程間的操作综看。
- 依賴時序的操作俩由。
- 不同數(shù)據(jù)之間存在綁定關系嫉髓。
- 沒有聲明是線程安全的劳跃。
多線程性能問題
線程調(diào)度
- 線程上下文切換
- CPU 緩存失效
- 鎖競爭谎仲、IO頻繁 會造成上下文切換的頻繁。
線程協(xié)作
- 線程間共享數(shù)據(jù)的頻繁Flush 刷盤(保證數(shù)據(jù)一致性)刨仑。
- 保證線程安全而取舍了CPU 對指令重排的優(yōu)化郑诺。
每個任務都創(chuàng)建一個線程帶來的問題
- 反復創(chuàng)建線程造成系統(tǒng)開銷比較大,線程創(chuàng)建和銷毀都需要時間杉武,如果任務是比較簡單的辙诞,那么創(chuàng)建和銷毀線程所消耗的資源可能比要處理的任務本身還要大,這是極其不合理的轻抱。
- 過多的線程會占用內(nèi)存資源飞涂,如過一個線程處理的任務耗時比較長,那么就會有大量的空閑線程處于線程饑餓 線程間上下文切換也會對CPU 帶來壓力祈搜, 同時也可能造成系統(tǒng)的不穩(wěn)定较店。
線程池解決線程資源過多的思路
- 線程池用固定數(shù)量的線程一直保持工作狀態(tài),執(zhí)行任務容燕。
- 根據(jù)需要創(chuàng)建線程泽西,控制線程的總數(shù)量,避免過多的線程占用資源缰趋。
線程池的優(yōu)點
- 線程池中的線程是可以復用的,避免了(創(chuàng)建陕见、銷毀)線程生命周期的系統(tǒng)開銷秘血。 線程池中的線程一直保持工作狀態(tài),可以直接處理任務评甜,避免了創(chuàng)建線程帶來的延遲灰粮。
- 線程池可以統(tǒng)籌內(nèi)存和CPU的使用,使資源可以合理分配利用忍坷。 線程池會根據(jù)配置和任務數(shù)量靈活控制線程數(shù)量粘舟。
- 線程池可以統(tǒng)一管理資源熔脂,可以統(tǒng)一協(xié)調(diào)和管理任務資源和線程。
線程池參數(shù)
參數(shù)名 | 含義 |
---|---|
corePoolSize | 核心線程數(shù) |
maxPoolSize | 最大可創(chuàng)建線程數(shù) |
KeepAliveTime | 空閑線程的存活時間 |
ThreadFactory | 線程工廠柑肴,定義創(chuàng)建新線程<br />的規(guī)則 |
workQueue | 用于存放任務的隊列 |
Handler | 按任務拒絕策略 處理被拒絕任務 |
線程池任務調(diào)度流程圖
線程池的特點
- 希望保持較少的線程數(shù)霞揉,且指在負載較大時才增加線程。
- 只有在任務阻塞隊列滿的情況下才在 corePoolSize 基礎上創(chuàng)建多的線程晰骑,如果采用無界隊列(LinkBlockQueue) 則永遠不會超出corePoolSize 的線程數(shù)限制适秩。
- corePoolSize 和 maxPoolSize 大小相同,創(chuàng)建固定大小的線程池硕舆。
- maxPoolSize 值設置 Integer.MAX_VALUE 創(chuàng)建更多的線程秽荞。
線程池拒絕任務的時機
- 程序調(diào)用 shutdown 方法關閉線程池,此時即使線程池中還有線程在處理任務抚官, 但新提交的任務仍被拒絕執(zhí)行扬跋。
- 線程池已經(jīng)飽和,任務阻塞隊列已滿凌节,線程數(shù)達到最大線程數(shù) maxPoolSize 上限钦听, 新提交的任務就會拒絕執(zhí)行。
線程池的拒絕策略
AbortPolicy: 拒絕策略在拒絕任務時會直接拋出 RejectedExecutionException 異常 刊咳, 可以捕獲異常并根據(jù)業(yè)務邏輯選擇重試或放棄提交彪见。
DiscardPolicy: 新任務被提交后直接被丟棄 也不會給任何異常通知, 風險比較大娱挨,可能造成無感知的數(shù)據(jù)丟失余指。
DiscardOldestPolicy: 丟棄掉任務隊列的中存活時間最長的任務,存在一定的數(shù)據(jù)丟失的風險跷坝。
-
CallerRunsPolicy: 把任務交于提交任務的線程執(zhí)行酵镜,誰提交任務誰負責執(zhí)行。
- 新提交的任務不會丟棄柴钻,保證了業(yè)務完整性和數(shù)據(jù)完整性淮韭。
- 提交任務的線程執(zhí)行新任務, 不會再提交任務給線程池贴届,線程池處理執(zhí)行任務隊列的任務靠粪, 騰出阻塞隊列空間。
常見的6種線程池
- FixedThreadPool
- CacheThreadPool
- ScheduledThreadPool
- SingleThreadExecutor
- SingleThreadScheduledExecutor
- ForkJoinPool
線程池 | corePoolSize | maxPoolSize | keepAliveTime |
---|---|---|---|
FixedThreadPool | 構(gòu)造函數(shù)傳入 | 同corePoolSize | 0 |
CacheThreadPool | 0 | Integer.MAX_VALUE | 60s |
ScheduledThreadPool | 構(gòu)造函數(shù)傳入 | Integer.MAX_VALUE | 0 |
SingleThreadExecutor | 1 | 1 | 0 |
SingleThreadScheduledExecutor | 1 | Integer.MAX_VALUE | 0 |
FixedThreadPool
核心線程數(shù)(corePoolSzie) 和最大線程數(shù)(maxPoolSize) 一樣毫蚓, 可以看做是固定線程數(shù)的線程池占键, 特點是線程池中的線程從0 開始增加,到corePoolSize 線程數(shù)上限元潘,就不再增加畔乙。
CacheThreadPool
可以稱為可緩存線程池, 它的線程數(shù)是幾乎可說是不設上限翩概,(Integer.MAX_VALUE 2^31-1),它的任務隊列是SynchronousQueue 隊列容量為0 牲距, 不存儲任務返咱,只負責任務的中轉(zhuǎn)與傳遞,效率比較高牍鞠。
當提交一個新任務時線程池會判斷是否存在空閑線程咖摹,如果有空閑線程就直接分配任務給線程去執(zhí)行,沒有則新建線程去執(zhí)行任務皮服。
ScheduledThreadPool
支持定時和周期性的執(zhí)行任務楞艾。
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
#1 每隔10s 鐘執(zhí)行一次任務。
service.schedule(new Task(), 10, TimeUnit.SECONDS);
#2 延遲10s 執(zhí)行第一次任務龄广,之后(從任務開始執(zhí)行時間計時) 每延遲10s 執(zhí)行一次任務硫眯。
service.scheduleAtFixedRate(new Task(), 10, 10, TimeUnit.SECONDS);
#3 延遲10s 執(zhí)行第一次任務,之后(從任務結(jié)束時間開始計時)每延遲10s 執(zhí)行一次任務择同。
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS);
SingleThreadExecutor
只存在一個線程去執(zhí)行任務两入,如果線程執(zhí)行任務過程中發(fā)生異常,線程池會創(chuàng)建新的線程去執(zhí)行后續(xù)的任務敲才。適合要求任務按提交順序執(zhí)行的場景裹纳。
SingleThreadScheduledExecutor
與 ThreadScheduledExecutor 類似,只是它的核心線程數(shù)設為了1紧武,只有一個線程去執(zhí)行任務剃氧。
ForkJoinPool
適用于遞歸場景,例如樹的遍歷阻星。朋鞍。
與上述線程池最大的不同點在于
-
適合執(zhí)行可以產(chǎn)生子線程的任務。比如一個任務Task 產(chǎn)生三個子任務 subTask, 那么三個子任務并行執(zhí)行互不影響妥箕,充分利用CPU 多核優(yōu)勢滥酥。 主任務執(zhí)行分為兩部分
- fork: 將任務分裂出子任務。
- join: 匯總子任務的執(zhí)行結(jié)果畦幢。
內(nèi)部結(jié)構(gòu)不同坎吻,上述線程池所有的線程公用一個任務隊列, 但是 ForkJoinPool 線程池中除了有一個公用的任務隊列外宇葱, 每個線程都自己獨立的雙端任務隊列 Deque瘦真。 線程分裂出來的子任務放入自己的Deque 任務隊列中,線程可以直接在直接的獨立隊列中獲取任務執(zhí)行(LIFO)黍瞧,減少了線程間競爭和切換吗氏。
work-stealing 當一個線程忙,而一個線程空閑時雷逆,空閑線程就會偷 任務放入自己的Deque 中執(zhí)行(FIFO)。
合適的線程數(shù)量
CPU 密集型
加密污尉、解密膀哲、壓縮往产、計算等一系列需要大量消耗CPU 資源的任務, 這樣的任務最大線程數(shù)為 CPU core*(1~2)某宪。 因為計算任務是比較繁重的仿村,會占用大量的CPU 資源, 申請過多線程容易造成線程的上下文切換兴喂,甚至會導致性能的下降蔼囊。
IO 密集型
數(shù)據(jù)庫數(shù)據(jù)讀寫、 文件內(nèi)容讀寫衣迷、網(wǎng)絡通信等任務畏鼓,這種任務的特點是并不會特別消耗CPU 資源,但是IO 操作耗時壶谒,會占用比較多的時間云矫。 線程數(shù)=CPU core * (1+ 平均等待時間/平均工作時間)。
線程池的關閉
shutdown()
- 安全關閉線程池汗菜, 調(diào)用 shutdown() 方法后并不是立即關閉線程池让禀,而是等正在執(zhí)行任務的線程 和 任務隊列里的任務執(zhí)行完畢后,再關閉陨界,
- 此時不在接受新提交的任務巡揍,新提交任務按拒絕策略拒絕掉。
shutdownNow()
執(zhí)行shutdownNow() 方法后菌瘪,會執(zhí)行以下步驟
- 給線程池中的所有線程發(fā)送 Interrupt 中斷信號腮敌,嘗試中斷任務的執(zhí)行。
- 將任務阻塞隊列里的任務轉(zhuǎn)移到一個列表list 返回麻车,可根據(jù)業(yè)務需求自行對返回的任務做后續(xù)的補救操作或記錄缀皱。
isShutdown()
檢測線程池是否已經(jīng)開始了關閉流程(執(zhí)行了shutdown() shutdownNow()), Boolean 類型,返回為true 也只是表明線程池開始了關閉工作动猬。
isTerminated()
檢測線程池是否已經(jīng)終結(jié)關閉掉啤斗,Boolean 類型,返回為true 意味者線程池中任務隊列里的任務都已經(jīng)執(zhí)行完畢,線程池被關閉赁咙。
awaitTermination()
用來判斷線程池狀態(tài)钮莲,在等待時間截止可能三種情況產(chǎn)生。
- 等待時間內(nèi)彼水,若線程池中所有提交的你任務都已執(zhí)行崔拥,線程池已關閉,返回true凤覆。
- 等待時間內(nèi)線程被中斷链瓦,會拋出 InterruptException()。
- 等待時間結(jié)束后,線程并未終結(jié)返回 false慈俯。
?
多線程的線程復用原理
通過 Wroker 的findTask 或 getTask 從 workqueue 中獲取待執(zhí)行的任務渤刃。
-
直接調(diào)用task 的 run 方法,執(zhí)行具體任務贴膘,不是新建線程卖子。
?
?
常見的各種鎖
鎖的7 大分類
- 偏向鎖/輕量級鎖/重量級鎖
- 可重入鎖/不可重入鎖
- 共享鎖/排他鎖
- 公平鎖/非公平鎖
- 悲觀鎖/樂觀鎖
- 自旋鎖/非自旋鎖
- 可中斷鎖/不可中斷鎖
偏向鎖/輕量級鎖/重量級鎖
對于synchronized 關鍵字加monitor 鎖的對象,在對象頭中標明鎖的狀態(tài)刑峡。
-
偏向鎖
如果自始至終洋闽,對于這把鎖都不存在競爭,那么沒必要上鎖突梦,只需要在對象頭的鎖標記位打一個標記就行诫舅,這是偏向鎖的思想,若對象初始化后沒有任何線程來獲取它的鎖阳似,那么它是可偏向的骚勘,當有第一個線程訪問并嘗試獲取鎖的時候,會將這個線程記錄下來撮奏, 之后嘗試獲取鎖的線程是偏向鎖的擁有者俏讹,那么就直接獲得鎖,開銷很小畜吊,性能好泽疆。
-
輕量級鎖
輕量級鎖是指當鎖原來是偏向鎖的時候,被另一個線程訪問玲献,說明存在鎖競爭殉疼,那么偏向鎖就會升級為輕量級鎖,線程會通過自旋+ CAS 的形式捌年,嘗試獲得鎖瓢娜,而不會陷入阻塞
-
重量級鎖
重量級鎖是互斥鎖,它是利用操作系統(tǒng)的同步機制實現(xiàn)的礼预,開銷很大眠砾, 當多個線程之間存在鎖的競爭,且線程任務執(zhí)行耗時比較長托酸,競爭的鎖就會長時間陷入自旋等待獲得鎖褒颈, JVM 處于對資源的平衡和合理利用, 這時鎖就會膨脹為重量級鎖励堡, 重量級鎖會讓其他申請卻拿不到鎖的線程進入阻塞狀態(tài)谷丸。
鎖升級
? 偏向鎖的性能最好,此時沒有出現(xiàn)多線程的競爭应结,輕量級鎖利用自旋+CAS 操作避免了重量級帶來的線程阻塞和喚醒刨疼,性能中等,重量級鎖則會把獲取不到線程的鎖阻塞,性能最差揩慕。
可重入鎖/不可重入鎖
- 可重入鎖:指的是線程當前持有這把鎖游两,在不釋放鎖情況下再次獲得這把鎖, 例如ReentrantLock
- 不可重入鎖: 雖然線程當前持有了這把鎖漩绵,也必須要釋放鎖后才能再次獲得這把鎖。
共享鎖/排他鎖
- 共享鎖: 可以被多個線程同時獲得肛炮,例如讀寫鎖中的讀鎖(Read Lock)止吐。
- 排他鎖: 鎖只能被一個線程持有,例如讀寫鎖中的寫鎖(Write Lock)侨糟。
公平鎖/非公品鎖
如果線程線程在嘗試獲取鎖的時候獲取不到鎖碍扔,就會陷入阻塞等待,開始排隊秕重,在等待隊列里等待長的優(yōu)先獲得鎖不同,先到先得,而非公平鎖在一定情況下會忽略掉正在排隊的線程溶耘,發(fā)生插隊現(xiàn)象二拐。
悲觀鎖/樂觀鎖
-
悲觀鎖:是指在獲取資源前必須先拿到鎖,以便達到 獨占狀態(tài)凳兵,當前線程在操作資源時百新, 其他線程拿不到鎖,不會影響當前線程的操作庐扫。
synchronized 關鍵字 Lock 相關接口
適用于并發(fā)寫入多饭望,臨界區(qū)業(yè)務復雜處理比較耗時,競爭激烈的場景形庭。
-
樂觀鎖: 它并不要求在獲取資源的前拿到鎖铅辞,也不會鎖住資源,相反樂觀鎖利用CAS 理念萨醒,在不獨占資源的情況下斟珊,完成對資源的修改。
原子類 AtomicInteger AtomicLong ..等
適用于讀多寫少验靡, 或 讀寫都很多倍宾,但是并發(fā)競爭不嚴重,臨界區(qū)任務處理較快等場景胜嗓,不加鎖的特定能大幅度提高性能高职。
自旋鎖/非自旋鎖
-
自旋鎖
如果線程現(xiàn)在拿不到鎖,并不會直接陷入阻塞也不會釋放CPU 資源辞州,而是循環(huán)不停的嘗試獲得鎖怔锌,這個過程就被形象的稱為自旋
自旋鎖用循環(huán)不停地嘗試獲得鎖,讓線程始終處于Runable 狀態(tài),節(jié)省了線程狀態(tài)切換(休眠 ->喚醒 恢復現(xiàn)場) 帶來的開銷
自旋鎖避免了線程狀態(tài)切換開銷埃元,但是因為不停地嘗試獲取同步資源的鎖涝涤,如果鎖一直不釋放,那頻繁的嘗試過程也是對處理器資源的浪費岛杀,甚至這種開銷在后期可能超過線程切換帶來的開銷阔拳。
適用于并發(fā)場景不是很高,臨界區(qū)程序比較簡單类嗤。
-
非自旋鎖
如果拿不到鎖就直接放棄糊肠,或進入阻塞排隊。
圖示
可中斷鎖/不可中斷鎖
- 不可中斷鎖:在java 中 synchorized 修飾的鎖是不可中斷鎖遗锣,一旦線程申請了鎖就只能等拿到鎖之后才能進行其他 的邏輯處理货裹。
- 可中斷鎖: 而 ReentranLock 是一種典型的可中斷鎖, 例如使用 lockInterruptibly 方法精偿,在申請獲取鎖的過程中弧圆,突然不想獲取了,那么也可以中斷之后去處理其他的任務笔咽。不用一直等待獲取鎖之后才能離開搔预。
synchronized vs Lock
相同點
- synchronized 和 Lock 都是用來保護資源線程訪問的安全。
- 都可以保證可見性
- synchronized 和 ReentrantLock 都擁有可重入的特點拓轻,都是可重入鎖斯撮。
不同點
- 用法上的區(qū)別
- 加解鎖的順序性不同
- synchronized 鎖不夠靈活
- synchronized 鎖只能同時被一個線程擁有,但是Lock 鎖沒有這個限制扶叉。
- 原理區(qū)別勿锅,sychronized 是內(nèi)置鎖,由JVM 實現(xiàn)獲取鎖和釋放鎖的原理枣氧。
synchronized 不能設置公平和非公平 - 性能區(qū)別
如何選擇
- 推薦使用JUC并發(fā)工具包溢十,不推薦使用 synchronized 和 Lock.
- 若synchronized 適合的程序,那么推薦使用synchronized 达吞,因為使用簡潔张弛,不容易出錯,(ReentrantLock 需要顯示的在 finally 塊中 lock.unlock 解鎖)
- 需要Lock 的特殊功能酪劫,比如可中斷吞鸭,公平鎖,非公平鎖等功能覆糟,才使用Lock刻剥。
JVM 對鎖的優(yōu)化
自適應自旋鎖
- jdk1.6 中引入了自適應自旋鎖來解決長時間自旋的問題,會根據(jù)最近自旋嘗試的成功率滩字、失敗率造虏、以及當前鎖的擁有者狀態(tài)等多種因素決定御吞,自旋的時間是變化的,比如最近嘗試自旋獲得鎖成功了漓藕,那么下次還會使用自旋陶珠,且允許更長時間的自旋,如果失敗了那可能會減少自旋時間享钞,甚至放棄自旋揍诽。
鎖粗化
- 把幾個同步代碼塊合并為一個,節(jié)省了頻繁加鎖解鎖的性能開銷栗竖,擴大了臨界區(qū)寝姿,適用于非循環(huán)的場景。
鎖消除
- 在經(jīng)過逃逸分析之后划滋,如果發(fā)現(xiàn)某些對象不可能被其他線程訪問到,那么就可以把它們當成棧上數(shù)據(jù)埃篓,棧上數(shù)據(jù)屬于本線程处坪,是線程安全的不需要加鎖,這樣就會自動把鎖消除掉架专。
偏向鎖/輕量級鎖/重量級鎖
參見上文