Android?線程死鎖場景與優(yōu)化解決
前言
線程死鎖是老生常談的問題,線程池死鎖本質(zhì)上屬于線程死鎖的一部分,線程池造成的死鎖問題往往和業(yè)務場景相關,當然更重要的是對線程池的理解不足,本文根據(jù)場景來說明一下常見的線程池死鎖問題,當然也會包含線程死鎖問題。
線程死鎖場景
死鎖的場景很多,有線程池相關,也有與線程相關,線程相關的線程池上往往也會出現(xiàn),反之卻不一定,本文會總結(jié)一些常見的場景,當然有些場景后續(xù)可能還需要補充。
經(jīng)典互斥關系死鎖
這種死鎖是最常見的經(jīng)典死鎖,假定存在 A、B 2 個任務,A 需要 B 的資源,B 需要 A 的資源,雙方都無法得到時便出現(xiàn)了死鎖,這種情況是鎖直接互相等待引發(fā),一般的情況下通過dumpheap 的lock hashcode就能發(fā)現(xiàn),相對來說容易定位的多。
//首先我們先定義兩個final的對象鎖.可以看做是共有的資源. final Object lockA = new Object(); final Object lockB = new Object(); //生產(chǎn)者A class ProductThreadA implements Runnable{ @Override public void run() { //這里一定要讓線程睡一會兒來模擬處理數(shù)據(jù) ,要不然的話死鎖的現(xiàn)象不會那么的明顯.這里就是同步語句塊里面,首先獲得對象鎖lockA,然后執(zhí)行一些代碼,隨后我們需要對象鎖lockB去執(zhí)行另外一些代碼. synchronized (lockA){ //這里一個log日志 Log.e("CHAO","ThreadA lock lockA"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB){ //這里一個log日志 Log.e("CHAO","ThreadA lock lockB"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //生產(chǎn)者B class ProductThreadB implements Runnable{ //我們生產(chǎn)的順序真好好生產(chǎn)者A相反,我們首先需要對象鎖lockB,然后需要對象鎖lockA. @Override public void run() { synchronized (lockB){ //這里一個log日志 Log.e("CHAO","ThreadB lock lockB"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockA){ //這里一個log日志 Log.e("CHAO","ThreadB lock lockA"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } } } //這里運行線程 ProductThreadA productThreadA = new ProductThreadA(); ProductThreadB productThreadB = new ProductThreadB(); Thread threadA = new Thread(productThreadA); Thread threadB = new Thread(productThreadB); threadA.start(); threadB.start();
這類問題需要進行排查和不斷的優(yōu)化,重點是優(yōu)化邏輯盡量減少鎖的使用,同時優(yōu)化調(diào)度機制。
Submit遞歸等待調(diào)用死鎖
原理是在固定的線程池數(shù)量中,不斷的 submit 任務,并且從工作線程通過get等待任務完成,
但是線程池數(shù)量是固定的,從頭到尾所有的線程沒執(zhí)行完成,某次 submit 時就沒有足夠的線程來處理任務,所有任務都處于等待。
ExecutorService pool = Executors.newSingleThreadExecutor(); //使用一個線程數(shù)模擬 pool.submit(() -> { try { log.info("First"); //上一個線程沒有執(zhí)行完,線程池沒有線程來提交本次任務,會處于等待狀態(tài) pool.submit(() -> log.info("Second")).get(); log.info("Third"); } catch (InterruptedException | ExecutionException e) { log.error("Error", e); } });
對于這種特殊邏輯,一定要思考清楚get方法調(diào)用的意義,如果僅僅為了串行執(zhí)行,使用一般隊列即可,當然你也可以join其他線程。
公用線程池線程 size 不足造成的死鎖
該類死鎖一般是把一個Size有限的線程池用于多個任務。
假定 A,B 兩個業(yè)務各需要2個線程處理生產(chǎn)者和消費者業(yè)務,且每個業(yè)務都有自己的lock,但是業(yè)務之間的lock沒有關聯(lián)關系。提供一個公共線程池,線程大小為2,顯然比較合理的執(zhí)行任務需要4個,或者至少3個,在線程數(shù)量不足的情況下這種情況下死鎖會高概率發(fā)生。
情形一:A,B 有序執(zhí)行,不會造成死鎖
情形二: A、B 并發(fā)執(zhí)行,造成死鎖
情形二出現(xiàn)的原因是 A,B 各分配了一個線程,當他們執(zhí)行的條件都不滿足的時處于要wait狀態(tài),這時線程池沒有更多的線程提供,將導致 A、B 處于死鎖。
因此,對于公用線程池的使用,Size不要設置過低,同時要盡可能避免加鎖和太耗時的任務,如果有加鎖和太耗時的需求,可以嘗試使用專用線程池。
RejectedExecutionHandler 使用不當造成的 “死鎖”
嚴格意義上不能稱為死鎖,但是這也是非常容易忽視的問題。原因在沒檢測線程池狀態(tài)的情況下,通過RejectionExectutionHandler回調(diào)方法中將任務重新加回去,如此往復循環(huán),鎖住Caller線程。
一般處理任務時,觸發(fā)該 RecjectedExecutionHandler 的情況分為 2 類,主要是 "線程池關閉"、“線程隊列和線程數(shù)已經(jīng)達到最大容量”,那么問題一般出現(xiàn)在前者,如果線程池 shutdown 關閉之后,我們嘗試在該 Handler 中重新加入任務到線程池,那么會造成死循環(huán)問題。
鎖住死循環(huán)
鎖住死循環(huán)本身也是一種死鎖,導致其他想獲取鎖資源的線程無法正常獲取中斷。
synchronized(lock){ while(true){ // do some slow things } }
這種循環(huán)鎖也是相當經(jīng)典,如果while內(nèi)部沒有wait的調(diào)用或者return或者break,那么這個鎖會一直存在。
文件鎖 & lock互斥
嚴格來說這種相對復雜,有可能是文件鎖與lock互斥,也有可能是多進程文件鎖獲取時阻塞之后無法釋放,導致java lock一直無法釋放,因此對于發(fā)生死鎖時,dumpheap時不要忽略文件操作相關的堆棧。
可見性不足
通常情況下,這不是死鎖,而是線程無限循環(huán),以至于該線程無法被其他任務使用,我們對一些線程循環(huán)會加一個變量標記其是否結(jié)束,但是如果可見性不足,也將無法造成退出的后果。
下面我們用主線程和普通線程模擬,我們在普通線程中修改變量A,但是A變量在主線程中可見性不足,導致主線程阻塞。
public class ThreadWatcher { public int A = 0; public static void main(String[] args) { final ThreadWatcher threadWatcher = new ThreadWatcher(); WorkThread t = new WorkThread(threadWatcher); t.start(); while (true) { if (threadWatcher.A == 1) { System.out.println("Main Thread exit"); break; } } } } class WorkThread extends Thread { private ThreadWatcher threadWatcher; public WorkThread(ThreadWatcher threadWatcher) { super(); this.threadWatcher = threadWatcher; } @Override public void run() { super.run(); System.out.println("sleep 1000"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.threadWatcher.A = 1; System.out.println("WorkThread exit"); } }
打印結(jié)果:
sleep 1000
WorkThread exit
由于A缺乏可見性,導致主線程一直循環(huán),這里有必要加上volatile或者使用atomic類,或者使用synchronized進行同步。注意,不能用final,final只能保證指令不可亂序,但不能保證可見性。
CountDownLatch 初始值過大
這個原因?qū)儆诰幊虇栴},比如需要2次countDown完成等待,而初始值為3次以上,必然導致等待的線程卡住。
CountDownLatch latch = new CountDownLatch(6); ExecutorService service = Executors.newFixedThreadPool(5); for(int i=0;i< 5;i++){ final int no = i+1; Runnable runnable=new Runnable(){ @Override public void run(){ try{ Thread.sleep((long)(Math.random()*10000)); System.out.println("No."+no+"準備好了。"); }catch(InterruptedException e){ e.printStackTrace(); }finally{ latch.countDown(); } } }; service.submit(runnable); } System.out.println("開始執(zhí)行....."); latch.await(); System.out.println("停止執(zhí)行");
實際上這種問題排查起來比較容易,對于計數(shù)式waiter,一定確保waiter能結(jié)束,即使發(fā)生異常行為。
線程死鎖優(yōu)化建議
死鎖一般和阻塞有關,對待死鎖問題,不妨換一種方式。
常見的優(yōu)化方法
1、可以有序執(zhí)行,當然這種也降低了并發(fā)優(yōu)勢
2、不要共用同一線程池,如果要共用,避免加鎖,阻塞和懸掛
3、使用公共鎖資源的 wait (long timeout) 機制,讓線程超時
4、如果過于擔心線程池不能回收,建議使用 keepaliveTime+allowCoreThreadTimeOut,回收線程但不影響線程狀態(tài),可以繼續(xù)提交任務。
5、必要時擴大線程池大小
公用線程任務移除
如果公共線程池正在執(zhí)行的線程阻塞了,那所有的任務需要等待,對于不重要的任務,可以選擇移除。
實際上正在執(zhí)行的線程任務很難去終止,公用線程池可能造成大量任務pending,但是從公用線程池中移除任務隊列顯然是比較危險的操作。一種可行的方法是warp task,每次添加runnable時記錄這些Task,退出特定業(yè)務時清理Warpper中的target目標任務
public class RemovableTask implements Runnable { private static final String TAG = "RemovableTask"; private Runnable target = null; private Object lock = new Object(); public RemovableTask(Runnable task) { this.target = task; } public static RemovableTask warp(Runnable r) { return new RemovableTask(r); } @Override public void run() { Runnable task; synchronized (this.lock) { task = this.target; } if (task == null) { MLog.d(TAG,"-cancel task-"); return; } task.run(); } public void dontRunIfPending() { synchronized (this.lock) { this.target = null; } } }
下面進行任務清理
public void purgHotSongRunnable() { for (RemovableTask r : pendingTaskLists){ r.dontRunIfPending(); } }
注意,這里仍然還可以利用享元模式優(yōu)化,減少RemovableTask的創(chuàng)建。
使用多路復用或協(xié)程
對于鎖比較厭惡的開發(fā)者可以使用多路復用或協(xié)程,這種情況下存避免不必要的等待,將wait轉(zhuǎn)化為notify,減少上下文切換,可以提高線程的執(zhí)行效率。
說到對協(xié)程觀點,一直存在爭議:
(1)協(xié)程是輕量級線程?但從cpu和系統(tǒng)角度,協(xié)程和多路復用都不是輕量級線程,CPU壓根不認識這貨,因此不可能比線程快,他只能加速線程的執(zhí)行,Okhttp也不是輕量級Socket,再快也快不過Socket,他們都是并發(fā)編程框架或者風格。
(2)kotlin也不是假協(xié)程,有觀點說kotlin會創(chuàng)建線程所以是假協(xié)程?epoll多路復用機制,難道所有任務都是epoll執(zhí)行的么?簡單的例子,從磁盤拷貝文件到內(nèi)存,雖然CPU不參與,但DMA也是芯片,毫無疑問,也算線程。協(xié)程在用戶態(tài)執(zhí)行耗時任務,如果不啟用線程,難不成要插入無數(shù)entry point 讓單個線程執(zhí)行一個任務?顯然,對于協(xié)程的認知,有人夸有人貶,主要原因還是是對于“框架”和執(zhí)行單元存在認知問題。
降低鎖粒度
JIT對鎖的優(yōu)化分為鎖消除和鎖重入,但是很難對鎖粒度進行優(yōu)化,因此,不要添加過大的代碼段顯然是必要的,因此有些耗時邏輯本身不涉及變量的修改,大可不必加鎖,只對修改變量的部分加鎖即可。
總結(jié)
本文主要是對死鎖的問題的優(yōu)化建議,至于性能問題,其實我們遵循一個原則:在保證流暢度的情況下線程越少越好。對于必要存在的線程,可以使用隊列緩沖、逃逸分析、對象標量化、鎖消除、鎖粗化、降低鎖范圍、多路復用、消除同步屏障、協(xié)程的角度去優(yōu)化。
到此這篇關于Android 線程死鎖場景與優(yōu)化解決的文章就介紹到這了,更多相關Android 線程死鎖 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android開發(fā)Intent跳轉(zhuǎn)傳遞list集合實現(xiàn)示例
這篇文章主要為大家介紹了Android開發(fā)Intent跳轉(zhuǎn)傳遞list集合實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07Flutter Http網(wǎng)絡請求實現(xiàn)詳解
這篇文章主要介紹了Flutter Http網(wǎng)絡請求實現(xiàn)詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04Android開發(fā)獲取系統(tǒng)中已安裝程序信息的方法
這篇文章主要介紹了Android開發(fā)獲取系統(tǒng)中已安裝程序信息的方法,可實現(xiàn)Android針對系統(tǒng)中已安裝程序名稱、路徑、大小、圖標、是否為系統(tǒng)app等信息的獲取功能,需要的朋友可以參考下2017-12-12Android App中使用ViewPager實現(xiàn)滑動分頁的要點解析
這篇文章主要介紹了Android App中使用ViewPager實現(xiàn)滑動分頁的要點解析,還附帶了一個禁止ViewPager左右滑動的方法,需要的朋友可以參考下2016-06-06Android短信驗證碼監(jiān)聽解決onChange多次調(diào)用的方法
本篇文章主要介紹了Android短信驗證碼監(jiān)聽解決onChange多次調(diào)用的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03Android自定義ViewGroup實現(xiàn)受邊界限制的滾動操作(3)
這篇文章主要為大家詳細介紹了Android自定義ViewGroup實現(xiàn)受邊界限制的滾動操作,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12Android創(chuàng)建簡單發(fā)送和接收短信應用
收發(fā)短信應該是每個手機最基本的功能之一了,即使是許多年前的老手機也都會具備這項功能,而Android 作為出色的智能手機操作系統(tǒng),自然也少不了在這方面的支持。今天我們開始自己創(chuàng)建一個簡單的發(fā)送和接收短信的應用,需要的朋友可以參考下2016-04-04