首页 > 其他分享 >Android 线程死锁场景与优化

Android 线程死锁场景与优化

时间:2023-12-30 21:31:39浏览次数:25  
标签:Thread lock 死锁 线程 new Android public

前言

线程死锁是老生常谈的问题,线程池死锁本质上属于线程死锁的一部分,线程池造成的死锁问题往往和业务场景相关,当然更重要的是对线程池的理解不足,本文根据场景来说明一下常见的线程池死锁问题,当然也会包含线程死锁问题。

线程死锁场景

死锁的场景很多,有线程池相关,也有与线程相关,线程相关的线程池上往往也会出现,反之却不一定,本文会总结一些常见的场景,当然有些场景后续可能还需要补充。

经典互斥关系死锁

这种死锁是最常见的经典死锁,假定存在 A、B 2 个任务,A 需要 B 的资源,B 需要 A 的资源,双方都无法得到时便出现了死锁,这种情况是锁直接互相等待引发,一般的情况下通过dumpheap 的lock hashcode就能发现,相对来说容易定位的多。

//首先我们先定义两个final的对象锁.可以看做是共有的资源.
    final Object lockA = new Object();
    final Object lockB = new Object();
//生产者A

class  ProductThreadA implements Runnable{
    @Override
    public void run() {
//这里一定要让线程睡一会儿来模拟处理数据 ,要不然的话死锁的现象不会那么的明显.这里就是同步语句块里面,首先获得对象锁lockA,然后执行一些代码,随后我们需要对象锁lockB去执行另外一些代码.
        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();
                }

            }
        }
    }
}
//生产者B
class  ProductThreadB implements Runnable{
    //我们生产的顺序真好好生产者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();

这类问题需要进行排查和不断的优化,重点是优化逻辑尽量减少锁的使用,同时优化调度机制。

Submit递归等待调用死锁

原理是在固定的线程池数量中,不断的 submit 任务,并且从工作线程通过get等待任务完成, 但是线程池数量是固定的,从头到尾所有的线程没执行完成,某次 submit 时就没有足够的线程来处理任务,所有任务都处于等待。

ExecutorService pool = Executors.newSingleThreadExecutor(); //使用一个线程数模拟
pool.submit(() -> {
        try {
            log.info("First");
             //上一个线程没有执行完,线程池没有线程来提交本次任务,会处于等待状态
            pool.submit(() -> log.info("Second")).get();
            log.info("Third");
        } catch (InterruptedException | ExecutionException e) {
           log.error("Error", e);
        }
   });

对于这种特殊逻辑,一定要思考清楚get方法调用的意义,如果仅仅为了串行执行,使用一般队列即可,当然你也可以join其他线程。

公用线程池线程 size 不足造成的死锁

该类死锁一般是把一个Size有限的线程池用于多个任务。

假定 A,B 两个业务各需要2个线程处理生产者和消费者业务,且每个业务都有自己的lock,但是业务之间的lock没有关联关系。提供一个公共线程池,线程大小为2,显然比较合理的执行任务需要4个,或者至少3个,在线程数量不足的情况下这种情况下死锁会高概率发生。

情形一:A,B 有序执行,不会造成死锁

情形二: A、B 并发执行,造成死锁

情形二出现的原因是 A,B 各分配了一个线程,当他们执行的条件都不满足的时处于要wait状态,这时线程池没有更多的线程提供,将导致 A、B 处于死锁。

因此,对于公用线程池的使用,Size不要设置过低,同时要尽可能避免加锁和太耗时的任务,如果有加锁和太耗时的需求,可以尝试使用专用线程池。

RejectedExecutionHandler 使用不当造成的 “死锁”

严格意义上不能称为死锁,但是这也是非常容易忽视的问题。原因在没检测线程池状态的情况下,通过RejectionExectutionHandler回调方法中将任务重新加回去,如此往复循环,锁住Caller线程。

一般处理任务时,触发该 RecjectedExecutionHandler 的情况分为 2 类,主要是 "线程池关闭"、“线程队列和线程数已经达到最大容量”,那么问题一般出现在前者,如果线程池 shutdown 关闭之后,我们尝试在该 Handler 中重新加入任务到线程池,那么会造成死循环问题。

锁住死循环

锁住死循环本身也是一种死锁,导致其他想获取锁资源的线程无法正常获取中断。

synchronized(lock){
  while(true){
   // do some slow things
  }
}

这种循环锁也是相当经典,如果while内部没有wait的调用或者return或者break,那么这个锁会一直存在。

文件锁 & lock互斥

严格来说这种相对复杂,有可能是文件锁与lock互斥,也有可能是多进程文件锁获取时阻塞之后无法释放,导致java lock一直无法释放,因此对于发生死锁时,dumpheap时不要忽略文件操作相关的堆栈。

可见性不足

通常情况下,这不是死锁,而是线程无限循环,以至于该线程无法被其他任务使用,我们对一些线程循环会加一个变量标记其是否结束,但是如果可见性不足,也将无法造成退出的后果。

下面我们用主线程和普通线程模拟,我们在普通线程中修改变量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");

    }
}

打印结果:

sleep 1000   
WorkThread exit

由于A缺乏可见性,导致主线程一直循环,这里有必要加上volatile或者使用atomic类,或者使用synchronized进行同步。注意,不能用final,final只能保证指令不可乱序,但不能保证可见性。

CountDownLatch 初始值过大

这个原因属于编程问题,比如需要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("开始执行.....");
latch.await();
System.out.println("停止执行");

实际上这种问题排查起来比较容易,对于计数式waiter,一定确保waiter能结束,即使发生异常行为。

线程死锁优化建议

死锁一般和阻塞有关,对待死锁问题,不妨换一种方式。

常见的优化方法

  • 1、可以有序执行,当然这种也降低了并发优势
  • 2、不要共用同一线程池,如果要共用,避免加锁,阻塞和悬挂
  • 3、使用公共锁资源的 wait (long timeout) 机制,让线程超时
  • 4、如果过于担心线程池不能回收,建议使用 keepaliveTime+allowCoreThreadTimeOut,回收线程但不影响线程状态,可以继续提交任务。
  • 5、必要时扩大线程池大小

公用线程任务移除

如果公共线程池正在执行的线程阻塞了,那所有的任务需要等待,对于不重要的任务,可以选择移除。

实际上正在执行的线程任务很难去终止,公用线程池可能造成大量任务pending,但是从公用线程池中移除任务队列显然是比较危险的操作。一种可行的方法是warp task,每次添加runnable时记录这些Task,退出特定业务时清理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();
    }
}

注意,这里仍然还可以利用享元模式优化,减少RemovableTask的创建。

使用多路复用或协程

对于锁比较厌恶的开发者可以使用多路复用或协程,这种情况下存避免不必要的等待,将wait转化为notify,减少上下文切换,可以提高线程的执行效率。

说到对协程观点,一直存在争议: (1)协程是轻量级线程?但从cpu和系统角度,协程和多路复用都不是轻量级线程,CPU压根不认识这货,因此不可能比线程快,他只能加速线程的执行,Okhttp也不是轻量级Socket,再快也快不过Socket,他们都是并发编程框架或者风格。

(2)kotlin也不是假协程,有观点说kotlin会创建线程所以是假协程?epoll多路复用机制,难道所有任务都是epoll执行的么?简单的例子,从磁盘拷贝文件到内存,虽然CPU不参与,但DMA也是芯片,毫无疑问,也算线程。协程在用户态执行耗时任务,如果不启用线程,难不成要插入无数entry point 让单个线程执行一个任务?显然,对于协程的认知,有人夸有人贬,主要原因还是是对于“框架”和执行单元存在认知问题。

降低锁粒度

JIT对锁的优化分为锁消除和锁重入,但是很难对锁粒度进行优化,因此,不要添加过大的代码段显然是必要的,因此有些耗时逻辑本身不涉及变量的修改,大可不必加锁,只对修改变量的部分加锁即可。

总结

本文主要是对死锁的问题的优化建议,至于性能问题,其实我们遵循一个原则:在保证流畅度的情况下线程越少越好。对于必要存在的线程,可以使用队列缓冲、逃逸分析、对象标量化、锁消除、锁粗化、降低锁范围、多路复用、消除同步屏障、协程的角度去优化。

标签:Thread,lock,死锁,线程,new,Android,public
From: https://blog.51cto.com/u_16175630/9042158

相关文章

  • 多线程循环打印123
    1、多线程循环打印123importjava.util.concurrent.locks.Condition;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;publicclassPrintThread{privateLocklock=newReentrantLock();privatevolatileintflag......
  • Android显示流程
    Android的显示过程具体包含这三部分:内容端(绘制)、SurfaceFlinger/Hwcomposer(合成)、DisplayProcessor及显示接口如LCD(显示) UE游戏的绘制过程: 绘制Application/Activity/ViewApplication包括4大组件:Activity、Service、Broadcast、ContentProviderActivity是带有生命周期......
  • 关于python3多线程和协程
    以下内容部分由chatgpt生成,本文仅作为备忘和记录。asyncio.sleep和time.sleep都是用于在Python中进行延迟操作的函数,但它们的工作方式和使用场景有一些不同。asyncio.sleep:asyncio.sleep是用于在异步代码中进行暂停的函数,它是asyncio模块中的一部分。在异步程序中......
  • nodejs多线程-共享内容
    前言:昨天遇到基于Nodejs启动多线程,以便不同服务之间可以调用(共享内存) worker_threadsnode官方文档注明了:worker_threads模块允许使用并行地执行JavaScript的线程。与child_process或cluster不同,worker_threads可以共享内存。它们通过传输ArrayBuffer实例或共享Sh......
  • Java线程池的学习
    线程池有如下四个优点:降低资源消耗: 重用已经创建的线程, 线程的创建和销毁需要消耗计算机资源,特别是在有大量创建线程请求且线程的处理过程是轻量级的,例如:大多数的服务器。提高响应速度:重用已经创建的线程。提高线程的稳定性:可创建的线程数量是由有限制的,限制值是有多个因素制约,例......
  • java-多线程编程
    多线程是指在一个程序中同时执行多个线程,每个线程都是独立运行的。Java中的多线程编程允许在同一个程序中同时执行多个任务,以提高程序的效率和响应性。以下是一些与Java多线程编程相关的重要概念:线程(Thread):线程是程序的执行单元,可以并发执行多个任务。在Java中,可以通过创建Thre......
  • mac安装appium(android/ios)
    一.(android)前提:1.安装java2.安装homebrew3.安装xcode4.安装git5.安装androidsdk官网https://www.androiddevtools.cn/  将下载的AndroidSDK解压,将得到如下目录。(具体安装步骤可以阅读SDKReadme.txt)阅读SDKReadme.txt文档得知:Inordertostartde......
  • 线程池中各个参数如何合理设置
    一、前言在开发过程中,好多场景要用到线程池。每次都是自己根据业务场景来设置线程池中的各个参数。这两天又有需求碰到了,索性总结一下方便以后再遇到可以直接看着用。虽说根据业务场景来设置各个参数的值,但有些万变不离其宗,掌握它的原理对如何用好线程池起了至关重要的作用。那我......
  • 2024年Android开发出路还能搞车载吗?
    前言众所周知今年互联网行业发展的并不愉快,导致互联网行业的就业形式不太理想,“开猿节流”的事情时有发生,于是不少Android开发萌生了转行做车载的想法。什么是车机开发?车机指的是安装在汽车里面的车载信息娱乐产品的简称,通俗点说就是我们在车内经常使用的收音机、音乐播放、地图导......
  • 想要在Android开发者中突出重围,性能优化必须了解一下
    前言众所周知,移动开发已经来到了后半场,为了能够在众多开发者中脱颖而出,我们需要对某一个领域有深入地研究与心得,对于Android开发者来说,目前,有几个好的细分领域值得我们去建立自己的技术壁垒,如下所示:1、性能优化专家:具备深度性能优化与体系化APM建设的能力。2、架构师:具有丰富的应用......