Android-Thread-Deadlock-Szenario und Optimierung

Vorwort

Thread-Deadlock ist ein alltägliches Problem. Der Thread-Pool-Deadlock ist im Wesentlichen ein Teil des Thread-Deadlocks. Durch Thread-Pools verursachte Deadlock-Probleme hängen oft mit Geschäftsszenarien zusammen. Wichtiger ist natürlich das mangelnde Verständnis von Thread-Pools. Dieser Artikel ist Basierend auf den Szenarien. Erklären Sie die häufigsten Thread-Pool-Deadlock-Probleme. Natürlich werden auch Thread-Deadlock-Probleme berücksichtigt.

Thread-Deadlock-Szenario

Es gibt viele Deadlock-Szenarien, einige beziehen sich auf Thread-Pools und andere auf Threads. Thread-bezogene Thread-Pools treten häufig auch auf, aber nicht unbedingt umgekehrt. In diesem Artikel werden einige häufige Szenarien zusammengefasst. Natürlich müssen einige Szenarien möglicherweise ergänzt werden später.

Klassischer Stillstand in gegenseitigen Ausschlussbeziehungen

Diese Art von Deadlock ist der häufigste klassische Deadlock. Angenommen, es gibt zwei Aufgaben, A und B. A benötigt die Ressourcen von B und B benötigt die Ressourcen von A. Wenn beide Parteien sie nicht erhalten können, tritt ein Deadlock auf. In diesem Fall die Sperre Direkt durch gegenseitiges Warten verursacht, kann es normalerweise über den Sperr-Hashcode von Dumpheap gefunden werden und ist relativ einfach zu finden.

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

Solche Probleme erfordern eine Untersuchung und kontinuierliche Optimierung. Der Schwerpunkt liegt auf der Optimierung der Logik zur Minimierung des Einsatzes von Sperren und der Optimierung des Planungsmechanismus.

Senden Sie einen rekursiven Wait-Call-Deadlock

Das Prinzip besteht darin, Aufgaben kontinuierlich in einer festen Anzahl von Thread-Pools zu übermitteln und darauf zu warten, dass die Aufgabe durch Abrufen vom Arbeitsthread abgeschlossen wird. Die Anzahl der Thread-Pools ist jedoch festgelegt. Wenn nicht alle Threads von Anfang bis Ende ausgeführt werden , es wird nicht genug für eine bestimmte Übermittlung geben. Threads werden zum Verarbeiten von Aufgaben verwendet, und alle Aufgaben warten.

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

Für diese spezielle Logik müssen Sie sich die Bedeutung des Get-Methodenaufrufs klar überlegen. Wenn es sich nur um eine serielle Ausführung handelt, verwenden Sie einfach eine allgemeine Warteschlange. Natürlich können Sie auch anderen Threads beitreten.

Deadlock aufgrund unzureichender Thread-Größe im öffentlichen Thread-Pool

Diese Art von Deadlock verwendet im Allgemeinen einen Thread-Pool mit begrenzter Größe für mehrere Aufgaben.

Nehmen Sie an, dass zwei Unternehmen, A und B, jeweils zwei Threads benötigen, um die Produzenten- und Verbrauchergeschäfte zu verarbeiten, und jedes Unternehmen über eine eigene Sperre verfügt, die Sperren zwischen den Unternehmen jedoch nicht miteinander verbunden sind. Stellen Sie einen öffentlichen Thread-Pool mit einer Thread-Größe von 2 bereit. Für eine vernünftigere Ausführungsaufgabe sind natürlich 4 oder mindestens 3 erforderlich. Wenn die Anzahl der Threads nicht ausreicht, kommt es mit hoher Wahrscheinlichkeit zu einem Deadlock.

Szenario 1: A und B werden der Reihe nach ausgeführt, ohne dass es zu einem Deadlock kommt.

Szenario 2: A und B werden gleichzeitig ausgeführt, was zu einem Deadlock führt

Der Grund für die zweite Situation besteht darin, dass A und B jeweils einem Thread zugewiesen sind. Wenn ihre Ausführungsbedingungen nicht erfüllt sind, befinden sie sich in einem Wartezustand. Zu diesem Zeitpunkt verfügt der Thread-Pool nicht über mehr Threads, die bereitgestellt werden können, was dazu führt A und B befinden sich in einer Sackgasse.

Daher sollte für die Verwendung öffentlicher Thread-Pools die Größe nicht zu niedrig eingestellt werden und Sperren und zu zeitaufwändige Aufgaben so weit wie möglich vermieden werden. Wenn Sperren und zu zeitaufwändige Aufgaben erforderlich sind, können Sie dies tun kann versuchen, einen dedizierten Thread-Pool zu verwenden.

„Deadlock“ durch unsachgemäße Verwendung von RejectedExecutionHandler

Streng genommen kann man nicht von einem Deadlock sprechen, aber es handelt sich auch um ein Problem, das sehr leicht zu ignorieren ist. Der Grund dafür ist, dass die Aufgabe über die Rückrufmethode RejectionExecutionHandler usw. wieder hinzugefügt wird, ohne den Status des Thread-Pools zu erkennen, wodurch der Aufrufer-Thread gesperrt wird.

Bei der allgemeinen Verarbeitung von Aufgaben werden die Situationen, die den RecjectedExecutionHandler auslösen, in zwei Kategorien unterteilt, hauptsächlich „Der Thread-Pool ist geschlossen“ und „Die Thread-Warteschlange und die Anzahl der Threads haben die maximale Kapazität erreicht“. Dann tritt das Problem im Allgemeinen in ersterem auf . Wenn der Thread-Pool heruntergefahren wird, führt der Versuch, in diesem Handler erneut Aufgaben zum Thread-Pool hinzuzufügen, zu einem Endlosschleifenproblem.

Endlosschleife sperren

Das Sperren einer Endlosschleife selbst ist ebenfalls ein Deadlock, der dazu führt, dass andere Threads, die Sperrressourcen erhalten möchten, keine Interrupts auf normale Weise erhalten können.

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

Diese Art von Schleifensperre ist ebenfalls recht klassisch. Wenn es in while keinen Aufruf zum Warten, Zurückkehren oder Unterbrechen gibt, ist diese Sperre immer vorhanden.

Dateisperre und Sperrmutex

Streng genommen ist dies relativ kompliziert: Möglicherweise schließen sich die Dateisperre und die Sperre gegenseitig aus, oder die Multiprozess-Dateisperre ist blockiert und kann nach dem Erwerb nicht freigegeben werden, was dazu führt, dass die Java-Sperre nicht funktioniert Wenn ein Deadlock auftritt, ignorieren Sie ihn daher nicht, wenn Sie den Stapel im Zusammenhang mit Dateivorgängen sichern.

Nicht genügend Sicht

Normalerweise handelt es sich hierbei nicht um einen Deadlock, sondern um eine Endlos-Thread-Schleife, sodass der Thread nicht von anderen Aufgaben verwendet werden kann. Wir fügen einigen Thread-Schleifen eine Variable hinzu, um zu markieren, ob sie endet. Wenn die Sichtbarkeit jedoch nicht ausreicht, wird dies nicht der Fall sein Ursache Ausgang. s Konsequenz.

Im Folgenden verwenden wir zur Simulation den Hauptthread und gewöhnliche Threads. Wir ändern die Variable A im gewöhnlichen Thread, aber die Sichtbarkeit der Variablen A im Hauptthread ist unzureichend, was dazu führt, dass der Hauptthread blockiert wird.

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

    }
}

Drucken Sie das Ergebnis aus:

sleep 1000   
WorkThread exit

Aufgrund der mangelnden Sichtbarkeit von A läuft der Hauptthread ständig in einer Schleife. Es ist notwendig, volatile hinzuzufügen oder die Atomklasse zu verwenden oder synchronisiert für die Synchronisierung zu verwenden. Beachten Sie, dass final nicht verwendet werden kann. Final kann nur sicherstellen, dass Anweisungen nicht fehlerhaft sind, kann jedoch nicht die Sichtbarkeit garantieren.

Der CountDownLatch-Anfangswert ist zu groß

Dieser Grund ist ein Programmierproblem. Beispielsweise muss countDown zweimal abgeschlossen werden, um die Wartezeit abzuschließen, und der Anfangswert beträgt mehr als dreimal, was unweigerlich dazu führt, dass der wartende Thread hängen bleibt.

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

Tatsächlich ist diese Art von Problem relativ einfach zu beheben. Stellen Sie beim Zählen von Kellnern sicher, dass der Kellner beenden kann, auch wenn ungewöhnliches Verhalten auftritt.

Vorschläge zur Thread-Deadlock-Optimierung

Deadlock hängt im Allgemeinen mit Blockieren zusammen. Um das Deadlock-Problem zu lösen, können Sie es auch auf andere Weise versuchen.

Gängige Optimierungsmethoden

  • 1. Es kann der Reihe nach ausgeführt werden, was natürlich auch den Parallelitätsvorteil verringert.
  • 2. Teilen Sie nicht denselben Thread-Pool. Wenn Sie ihn teilen möchten, vermeiden Sie Sperren, Blockieren und Hängen.
  • 3. Verwenden Sie den Wartemechanismus (langes Timeout) öffentlicher Sperrressourcen, um Threads eine Zeitüberschreitung zu ermöglichen.
  • 4. Wenn Sie zu sehr befürchten, dass der Thread-Pool nicht recycelt werden kann, wird empfohlen, keepaliveTime+allowCoreThreadTimeOut zu verwenden, um den Thread zu recyceln, ohne jedoch den Thread-Status zu beeinträchtigen, und Sie können weiterhin Aufgaben senden.
  • 5. Erweitern Sie bei Bedarf die Thread-Poolgröße

Entfernung der öffentlichen Thread-Aufgabe

Wenn der vom öffentlichen Thread-Pool ausgeführte Thread blockiert ist, müssen alle Aufgaben warten. Bei unwichtigen Aufgaben können Sie diese entfernen.

Tatsächlich ist es schwierig, die ausgeführten Thread-Aufgaben zu beenden. Der öffentliche Thread-Pool kann eine große Anzahl ausstehender Aufgaben verursachen, aber das Entfernen der Aufgabenwarteschlange aus dem öffentlichen Thread-Pool ist offensichtlich ein gefährlicherer Vorgang. Eine mögliche Methode besteht darin, Aufgaben zu warpen, diese Aufgaben jedes Mal aufzuzeichnen, wenn eine ausführbare Datei hinzugefügt wird, und die Zielaufgaben im Warpper zu bereinigen, wenn ein bestimmtes Unternehmen verlassen wird

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

Bereinigen Sie die folgenden Aufgaben

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

Beachten Sie, dass die Optimierung des Fliegengewichtsmodus hier weiterhin verwendet werden kann, um die Erstellung abnehmbarer Aufgaben zu reduzieren.

Verwenden Sie Multiplexing oder Coroutinen

Entwickler, die Sperren ablehnen, können Multiplexing oder Coroutinen verwenden. In diesem Fall kann unnötiges Warten vermieden, Warten in Benachrichtigung umgewandelt, Kontextwechsel reduziert und die Effizienz der Thread-Ausführung verbessert werden.

Wenn es um die Sichtweise von Coroutinen geht, gab es schon immer Kontroversen:
(1) Sind Coroutinen leichte Threads? Aus Sicht der CPU und des Systems sind Coroutinen und Multiplexer jedoch keine leichten Threads. Die CPU erkennt sie überhaupt nicht, daher können sie nicht schneller als Threads sein. Sie können nur die Ausführung von Threads beschleunigen. Okhttp ist kein Leichtgewicht Socket auch nicht. Egal wie schnell es ist, es kann nicht schneller als Socket sein. Es handelt sich bei allen um gleichzeitige Programmierframeworks oder -stile.

(2) Kotlin ist keine gefälschte Coroutine. Einige Leute sagen, dass Kotlin Threads erstellt, es also eine gefälschte Coroutine ist? Epoll-Multiplex-Mechanismus, werden alle Aufgaben von Epoll ausgeführt? Ein einfaches Beispiel ist das Kopieren von Dateien von der Festplatte in den Speicher. Obwohl die CPU nicht beteiligt ist, ist DMA ebenfalls ein Chip und wird zweifellos als Thread betrachtet. Coroutinen führen zeitaufwändige Aufgaben im Benutzermodus aus. Wenn Threads nicht aktiviert sind, ist es dann möglich, unzählige Einstiegspunkte einzufügen, damit ein einzelner Thread eine Aufgabe ausführen kann? Offensichtlich loben und kritisieren einige Leute das Verständnis von Coroutinen. Der Hauptgrund dafür ist, dass es kognitive Probleme mit dem „Framework“ und den Ausführungseinheiten gibt.

Reduzieren Sie die Granularität der Sperre

Die Optimierung von Sperren durch JIT ist in Sperrenbeseitigung und Sperrenwiedereintritt unterteilt, es ist jedoch schwierig, die Sperrgranularität zu optimieren. Daher ist es offensichtlich notwendig, nicht zu große Codesegmente hinzuzufügen. Daher erfordert einige zeitaufwändige Logik selbst keine Änderung von Variablen. Es besteht keine Notwendigkeit, sie zu sperren, sondern nur den Teil zu sperren, der die Variablen ändert.

Zusammenfassen

In diesem Artikel geht es hauptsächlich um Optimierungsvorschläge für Deadlock-Probleme. Bei Leistungsproblemen folgen wir eigentlich einem Prinzip: Je weniger Threads, desto besser und gleichzeitig eine reibungslose Funktion gewährleisten. Für erforderliche Threads können Sie zur Optimierung Warteschlangenpufferung, Escape-Analyse, Objektskalarisierung, Sperrenbeseitigung, Sperrenvergröberung, Sperrbereichsreduzierung, Multiplexing, Synchronisierungsbarrierebeseitigung und Coroutine-Perspektive verwenden.

zu guter Letzt

Wenn Sie Architekt werden oder die Gehaltsspanne von 20.000 bis 30.000 durchbrechen möchten, beschränken Sie sich nicht auf Programmieren und Wirtschaft, sondern müssen in der Lage sein, Ihr Programmierdenken auszuwählen, zu erweitern und zu verbessern. Darüber hinaus ist auch eine gute Karriereplanung sehr wichtig und Lerngewohnheiten sind wichtig, aber das Wichtigste ist, durchhalten zu können. Jeder Plan, der nicht konsequent umgesetzt werden kann, ist leeres Gerede.

Wenn Sie keine Anleitung haben, finden Sie hier eine Reihe von „Erweiterten Hinweisen zu den acht Modulen von Android“, die von einem leitenden Architekten bei Alibaba verfasst wurden und Ihnen dabei helfen sollen, unordentliches, verstreutes und fragmentiertes Wissen systematisch zu organisieren, damit Sie verschiedene systematisch und effizient beherrschen können Wissenspunkte der Android-Entwicklung.
Bild
Im Vergleich zu den fragmentierten Inhalten, die wir normalerweise lesen, sind die Wissenspunkte in dieser Notiz systematischer, leichter zu verstehen und zu merken und streng nach dem Wissenssystem geordnet.

Begrüßen Sie alle mit einem Klick und drei Links zum Support. Wenn Sie die Informationen im Artikel benötigen, scannen Sie einfach die offizielle CSDN-Zertifizierungs-WeChat-Karte am Ende des Artikels, um sie kostenlos zu erhalten↓↓↓ (Es gibt auch einen kleinen Bonus von ChatGPT-Roboter am Ende des Artikels, nicht verpassen)

PS: Es gibt auch einen ChatGPT-Roboter in der Gruppe, der alle Arbeits- oder technischen Fragen beantworten kann.

Bild

Ich denke du magst

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