Android thread deadlock scenario and optimization

Preface

Thread deadlock is a commonplace problem. Thread pool deadlock is essentially a part of thread deadlock. Deadlock problems caused by thread pools are often related to business scenarios. Of course, the more important thing is the lack of understanding of thread pools. This article is based on the scenarios. Explain the common thread pool deadlock problems, of course, thread deadlock problems will also be included.

Thread deadlock scenario

There are many deadlock scenarios, some related to thread pools and some related to threads. Thread-related thread pools often also appear, but not necessarily vice versa. This article will summarize some common scenarios. Of course, some scenarios may need to be supplemented later.

Classic mutual exclusion relationship deadlock

This kind of deadlock is the most common classic deadlock. Assume that there are two tasks, A and B. A needs B's resources, and B needs A's resources. When both parties cannot obtain them, a deadlock occurs. In this case, the lock directly Caused by waiting for each other, usually it can be found through the lock hashcode of dumpheap, and it is relatively easy to locate.

    //首先我们先定义两个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();

Such problems require investigation and continuous optimization. The focus is on optimizing logic to minimize the use of locks and optimizing the scheduling mechanism.

Submit recursive wait call deadlock

The principle is to continuously submit tasks in a fixed number of thread pools, and wait for the task to be completed through get from the working thread. However, the number of thread pools is fixed. If all threads are not executed from beginning to end, there will not be enough for a certain submission. Threads are used to process tasks, and all tasks are waiting.

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);
        }
   });

For this special logic, you must think clearly about the meaning of the get method call. If it is just for serial execution, just use a general queue. Of course, you can also join other threads.

Deadlock caused by insufficient thread size in the public thread pool

This type of deadlock generally uses a thread pool with a limited size for multiple tasks.

Assume that two businesses, A and B, each require 2 threads to process the producer and consumer businesses, and each business has its own lock, but the locks between the businesses are not related. Provide a public thread pool with a thread size of 2. Obviously, a more reasonable execution task requires 4, or at least 3. When the number of threads is insufficient, deadlock will occur with a high probability.

Scenario 1: A and B are executed in order without causing deadlock.

Scenario 2: A and B execute concurrently, causing deadlock

The reason for the second situation is that A and B are each assigned a thread. When their execution conditions are not met, they are in a wait state. At this time, the thread pool does not have more threads to provide, which will cause A and B to be in deadlock.

Therefore, for the use of public thread pools, the Size should not be set too low, and locking and too time-consuming tasks should be avoided as much as possible. If there is a need for locking and too time-consuming tasks, you can try to use a dedicated thread pool.

"Deadlock" caused by improper use of RejectedExecutionHandler

Strictly speaking, it cannot be called a deadlock, but this is also a problem that is very easy to ignore. The reason is that without detecting the status of the thread pool, the task is added back through the RejectionExecutionHandler callback method, and so on, locking the Caller thread.

When generally processing tasks, the situations that trigger the RecjectedExecutionHandler are divided into two categories, mainly "the thread pool is closed" and "the thread queue and the number of threads have reached the maximum capacity". Then the problem generally occurs in the former. If the thread pool shuts down, we Trying to re-add tasks to the thread pool in this Handler will cause an infinite loop problem.

lock infinite loop

Locking an infinite loop itself is also a deadlock, causing other threads that want to obtain lock resources to be unable to obtain interrupts normally.

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

This kind of loop lock is also quite classic. If there is no call to wait or return or break inside while, then this lock will always exist.

File lock & lock mutex

Strictly speaking, this is relatively complicated. It may be that the file lock and the lock are mutually exclusive, or it may be that the multi-process file lock is blocked and cannot be released after being acquired, resulting in the java lock being unable to be released. Therefore, when a deadlock occurs, do not ignore it when dumping Stack related to file operations.

Not enough visibility

Usually, this is not a deadlock, but an infinite thread loop, so that the thread cannot be used by other tasks. We will add a variable to some thread loops to mark whether it ends, but if the visibility is insufficient, it will not cause exit. s consequence.

Below we use the main thread and ordinary threads to simulate. We modify variable A in the ordinary thread, but the visibility of variable A in the main thread is insufficient, causing the main thread to block.

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");

    }
}

Print the result:

sleep 1000   
WorkThread exit

Due to the lack of visibility of A, the main thread keeps looping. It is necessary to add volatile or use the atomic class, or use synchronized for synchronization. Note that final cannot be used. Final can only ensure that instructions are not out of order, but it cannot guarantee visibility.

CountDownLatch initial value is too large

This reason is a programming problem. For example, countDown needs to be completed twice to complete the wait, and the initial value is more than three times, which will inevitably cause the waiting thread to get stuck.

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("停止执行");

In fact, this kind of problem is relatively easy to troubleshoot. For counting waiters, make sure that the waiter can end, even if abnormal behavior occurs.

Thread deadlock optimization suggestions

Deadlock is generally related to blocking. To deal with the deadlock problem, you might as well try another way.

Common optimization methods

  • 1. It can be executed in order. Of course, this also reduces the concurrency advantage.
  • 2. Do not share the same thread pool. If you want to share, avoid locking, blocking and hanging.
  • 3. Use the wait (long timeout) mechanism of public lock resources to allow threads to time out.
  • 4. If you are too worried about the thread pool not being able to recycle, it is recommended to use keepaliveTime+allowCoreThreadTimeOut to recycle the thread but not affect the thread status, and you can continue to submit tasks.
  • 5. Expand the thread pool size if necessary

Public thread task removal

If the thread being executed by the public thread pool is blocked, all tasks need to wait. For unimportant tasks, you can choose to remove them.

In fact, it is difficult to terminate the thread tasks being executed. The public thread pool may cause a large number of pending tasks, but removing the task queue from the public thread pool is obviously a more dangerous operation. One possible method is to warp tasks, record these tasks each time a runnable is added, and clean up the target tasks in the Warpper when exiting a specific business

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;
        }
    }
}

Clean up the tasks below

public void purgHotSongRunnable() {
    
    
    for (RemovableTask r : pendingTaskLists){
    
    
        r.dontRunIfPending();
    }
}

Note that flyweight mode optimization can still be used here to reduce the creation of RemovableTasks.

Use multiplexing or coroutines

Developers who are averse to locks can use multiplexing or coroutines. In this case, unnecessary waiting can be avoided, wait can be converted into notify, context switching can be reduced, and thread execution efficiency can be improved.

When it comes to the view of coroutines, there has always been controversy:
(1) Are coroutines lightweight threads? But from the perspective of the CPU and the system, coroutines and multiplexers are not lightweight threads. The CPU does not recognize them at all, so they cannot be faster than threads. They can only accelerate the execution of threads. Okhttp is not a lightweight Socket either. , no matter how fast it is, it cannot be faster than Socket. They are all concurrent programming frameworks or styles.

(2) Kotlin is not a fake coroutine. Some people say that kotlin creates threads, so it is a fake coroutine? epoll multiplexing mechanism, are all tasks executed by epoll? A simple example is copying files from disk to memory. Although the CPU is not involved, DMA is also a chip and is undoubtedly considered a thread. Coroutines perform time-consuming tasks in user mode. If threads are not enabled, is it possible to insert countless entry points to allow a single thread to perform a task? Obviously, some people praise and criticize the understanding of coroutines. The main reason is that there are cognitive problems with the "framework" and execution units.

Reduce lock granularity

JIT's optimization of locks is divided into lock elimination and lock reentrancy, but it is difficult to optimize lock granularity. Therefore, it is obviously necessary not to add too large code segments. Therefore, some time-consuming logic itself does not involve the modification of variables. There is no need to lock, just lock the part that modifies the variables.

Summarize

This article is mainly about optimization suggestions for deadlock problems. As for performance issues, we actually follow a principle: the fewer threads, the better while ensuring smoothness. For necessary threads, you can use queue buffering, escape analysis, object scalarization, lock elimination, lock coarsening, lock range reduction, multiplexing, synchronization barrier elimination, and coroutine perspective to optimize.

at last

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, you must be able to select and expand, and improve your programming thinking. In addition, good career planning is also very important, and learning habits are important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here is a set of "Advanced Notes on the Eight Modules of Android" written by a senior architect at Alibaba to help you systematically organize messy, scattered, and fragmented knowledge, so that you can systematically and efficiently Master various knowledge points of Android development.
img
Compared with the fragmented content we usually read, the knowledge points in this note are more systematic, easier to understand and remember, and are strictly arranged according to the knowledge system.

Welcome everyone to support with one click and three links. If you need the information in the article, just scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓ (There is also a small bonus of ChatGPT robot at the end of the article, don’t miss it)

PS: There is also a ChatGPT robot in the group, which can answer everyone’s work or technical questions.

picture

Guess you like

Origin blog.csdn.net/Androiddddd/article/details/135309799