JAVA 多線程與鎖

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)度流程圖

soket Program-線程池任務處理.png

線程池的特點

  • 希望保持較少的線程數(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)谷丸。

  • 鎖升級

soket Program-鎖升級.jpg

? 偏向鎖的性能最好,此時沒有出現(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ū)程序比較簡單类嗤。

  • 非自旋鎖

    如果拿不到鎖就直接放棄糊肠,或進入阻塞排隊。

  • 圖示

soket Program-自旋鎖非自旋鎖.jpg
可中斷鎖/不可中斷鎖
  • 不可中斷鎖:在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ù)屬于本線程处坪,是線程安全的不需要加鎖,這樣就會自動把鎖消除掉架专。
偏向鎖/輕量級鎖/重量級鎖

參見上文

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末同窘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子部脚,更是在濱河造成了極大的恐慌想邦,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件委刘,死亡現(xiàn)場離奇詭異丧没,居然都是意外死亡,警方通過查閱死者的電腦和手機锡移,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門呕童,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淆珊,你說我怎么就攤上這事夺饲。” “怎么了施符?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵往声,是天一觀的道長。 經(jīng)常有香客問我戳吝,道長浩销,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任骨坑,我火速辦了婚禮撼嗓,結(jié)果婚禮上柬采,老公的妹妹穿的比我還像新娘。我一直安慰自己且警,他們只是感情好粉捻,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著斑芜,像睡著了一般肩刃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上杏头,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天盈包,我揣著相機與錄音,去河邊找鬼醇王。 笑死呢燥,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的寓娩。 我是一名探鬼主播叛氨,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼棘伴!你這毒婦竟也來了寞埠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤焊夸,失蹤者是張志新(化名)和其女友劉穎仁连,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阱穗,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡饭冬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了揪阶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伍伤。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖遣钳,靈堂內(nèi)的尸體忽然破棺而出扰魂,到底是詐尸還是另有隱情,我是刑警寧澤蕴茴,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布劝评,位于F島的核電站,受9級特大地震影響倦淀,放射性物質(zhì)發(fā)生泄漏蒋畜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一撞叽、第九天 我趴在偏房一處隱蔽的房頂上張望姻成。 院中可真熱鬧插龄,春花似錦、人聲如沸科展。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽才睹。三九已至徘跪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琅攘,已是汗流浹背垮庐。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留坞琴,地道東北人哨查。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像剧辐,于是被迫代替她去往敵國和親解恰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361