欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android?線程死鎖場景與優(yōu)化解決

 更新時間:2023年12月29日 11:07:10   作者:時光少年  
線程死鎖是老生常談的問題,線程池死鎖本質(zhì)上屬于線程死鎖的一部分,線程池造成的死鎖問題往往和業(yè)務場景相關,本文主要介紹了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ù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論