Ausführliche Erklärung des Java-Thread-Pools, dies ist möglicherweise der beste Artikel

Wenn wir im vorherigen Artikel Threads verwenden, erstellen wir einen Thread, der sehr einfach zu implementieren ist, aber es gibt ein Problem:

Wenn es eine große Anzahl gleichzeitiger Threads gibt und jeder Thread mit der Ausführung einer Aufgabe für kurze Zeit endet, wird das häufige Erstellen von Threads die Effizienz des Systems stark verringern, da das häufige Erstellen und Zerstören von Threads Zeit in Anspruch nimmt.

Gibt es also eine Möglichkeit, Threads wiederverwendbar zu machen, d. h. nach dem Ausführen einer Aufgabe wird sie nicht zerstört, sondern kann weiterhin andere Aufgaben ausführen?

In Java kann dieser Effekt durch einen Thread-Pool erreicht werden. Heute erklären wir den Thread-Pool von Java im Detail: Zuerst beginnen wir mit den Methoden in der Kernklasse ThreadPoolExecutor, beschreiben dann das Implementierungsprinzip, geben dann ein Beispiel für ihre Verwendung und diskutieren schließlich, wie man sie sinnvoll konfiguriert Größe des Threadpools.

1. Die ThreadPoolExecutor-Klasse in Java

Die Klasse java.uitl.concurrent.ThreadPoolExecutor ist die Kernklasse im Thread-Pool. Wenn Sie also den Thread-Pool in Java gründlich verstehen möchten, müssen Sie zuerst diese Klasse verstehen. Werfen wir einen Blick auf den spezifischen Implementierungsquellcode der ThreadPoolExecutor-Klasse.

In der ThreadPoolExecutor-Klasse werden vier Konstruktoren bereitgestellt:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

Wie aus dem obigen Code ersichtlich ist, erbt ThreadPoolExecutor die AbstractExecutorService-Klasse und stellt vier Konstruktoren bereit.Tatsächlich wird durch Beobachtung der spezifischen Implementierung des Quellcodes jedes Konstruktors festgestellt, dass die ersten drei Konstruktoren der vierte Konstruktor sind, der als Initialisierungsarbeit bezeichnet wird vom Gerät durchgeführt.

Im Folgenden wird die Bedeutung der einzelnen Parameter im Konstruktor erläutert:

  • corePoolSize: Die Größe des Kernpools, dieser Parameter hat eine große Beziehung zum später beschriebenen Implementierungsprinzip des Threadpools. Nachdem der Thread-Pool erstellt wurde, befinden sich standardmäßig keine Threads im Thread-Pool, aber warten Sie auf das Eintreffen einer Aufgabe, bevor Sie einen Thread zum Ausführen der Aufgabe erstellen, es sei denn, die Methode prestartAllCoreThreads() oder prestartCoreThread() wird von diesen aufgerufen Zwei Methoden Wie Sie dem Namen entnehmen können, bedeutet dies, Threads vorab zu erstellen, dh CorePoolSize-Threads oder einen Thread zu erstellen, bevor keine Tasks eintreffen. Nachdem der Thread-Pool erstellt wurde, ist die Anzahl der Threads im Thread-Pool standardmäßig 0. Wenn eine Aufgabe eintrifft, wird ein Thread erstellt, um die Aufgabe auszuführen. Wenn die Anzahl der Threads im Thread-Pool corePoolSize erreicht, wird dies der Fall sein gesendet werden an Die Aufgaben werden in die Cache-Warteschlange gestellt;
  • maximumPoolSize: Die maximale Anzahl von Threads im Thread-Pool, dieser Parameter ist auch ein sehr wichtiger Parameter, er gibt an, wie viele Threads im Thread-Pool maximal erstellt werden können;
  • keepAliveTime: Gibt an, wie lange der Thread beendet wird, wenn keine Taskausführung stattfindet. Standardmäßig funktioniert keepAliveTime nur, wenn die Anzahl der Threads im Thread-Pool größer als corePoolSize ist, bis die Anzahl der Threads im Thread-Pool nicht größer als corePoolSize ist, d. h. wenn die Anzahl der Threads im Thread-Pool größer ist als corePoolSize, wenn ein Thread im Leerlauf ist Wenn die Zeit keepAliveTime erreicht, wird er beendet, bis die Anzahl der Threads im Thread-Pool corePoolSize nicht überschreitet. Wenn jedoch die Methode allowCoreThreadTimeOut(boolean) aufgerufen wird und die Anzahl der Threads im Thread-Pool nicht größer als corePoolSize ist, funktioniert der Parameter keepAliveTime ebenfalls, bis die Anzahl der Threads im Thread-Pool 0 beträgt;
  • unit: Die Zeiteinheit des Parameters keepAliveTime, es gibt 7 Werte und es gibt 7 statische Eigenschaften in der Klasse TimeUnit:
TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒
  • workQueue: Eine blockierende Warteschlange, in der Aufgaben gespeichert werden, die auf ihre Ausführung warten. Die Wahl dieses Parameters ist ebenfalls sehr wichtig und hat einen erheblichen Einfluss auf den laufenden Prozess des Thread-Pools. Generell hat die blockierende Warteschlange hier die folgenden Optionen:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;

ArrayBlockingQueue und PriorityBlockingQueue werden weniger verwendet, und LinkedBlockingQueue und Synchronous werden im Allgemeinen verwendet. Die Warteschlangenstrategie des Thread-Pools hängt mit BlockingQueue zusammen.

  • threadFactory: Thread-Fabrik, die hauptsächlich zum Erstellen von Threads verwendet wird;
  • handler: Gibt die Strategie an, wenn die Bearbeitung einer Aufgabe verweigert wird, mit den folgenden vier Werten:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

 Die Beziehung zwischen der Konfiguration bestimmter Parameter und dem Thread-Pool wird im nächsten Abschnitt beschrieben.

Aus dem oben angegebenen Code der ThreadPoolExecutor-Klasse können wir erkennen, dass ThreadPoolExecutor AbstractExecutorService erbt.Lassen Sie uns einen Blick auf die Implementierung von AbstractExecutorService werfen:

public abstract class AbstractExecutorService implements ExecutorService {
 
     
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { };
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { };
    public Future<?> submit(Runnable task) {};
    public <T> Future<T> submit(Runnable task, T result) { };
    public <T> Future<T> submit(Callable<T> task) { };
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                            boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
    };
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    };
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
    };
}

AbstractExecutorService ist eine abstrakte Klasse, die die ExecutorService-Schnittstelle implementiert.

Schauen wir uns die Implementierung der ExecutorService-Schnittstelle an:

public interface ExecutorService extends Executor {
 
    void shutdown();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Und ExecutorService erbt die Executor-Schnittstelle. Werfen wir einen Blick auf die Implementierung der Executor-Schnittstelle:

public interface Executor {
    void execute(Runnable command);
}

An dieser Stelle sollten Sie die Beziehung zwischen ThreadPoolExecutor, AbstractExecutorService, ExecutorService und Executor verstehen.

Executor ist ein Top-Level-Interface, in dem nur eine Methode execute(Runnable) deklariert ist, der Rückgabewert void ist und der Parameter vom Typ Runnable ist, was wörtlich zu verstehen ist, dass es zur Ausführung der Aufgaben verwendet wird ging hinein;

Dann erbt die ExecutorService-Schnittstelle die Executor-Schnittstelle und deklariert einige Methoden: submit, invokeAll, invokeAny, shutdown usw.;

Die abstrakte Klasse AbstractExecutorService implementiert die ExecutorService-Schnittstelle und implementiert grundsätzlich alle in ExecutorService deklarierten Methoden;

Dann erbt ThreadPoolExecutor die Klasse AbstractExecutorService.

Es gibt mehrere sehr wichtige Methoden in der ThreadPoolExecutor-Klasse:

execute()
submit()
shutdown()
shutdownNow()

Die execute()-Methode ist eigentlich eine in Executor deklarierte Methode, die speziell in ThreadPoolExecutor implementiert ist. Diese Methode ist die Kernmethode von ThreadPoolExecutor. Durch diese Methode kann eine Aufgabe an den Thread-Pool zur Ausführung durch den Thread-Pool übermittelt werden.

Die Methode submit() ist eine in ExecutorService deklarierte Methode. Sie wurde in AbstractExecutorService implementiert. Sie wird in ThreadPoolExecutor nicht neu geschrieben. Diese Methode wird auch verwendet, um Aufgaben an den Thread-Pool zu senden, unterscheidet sich jedoch von der Methode execute() ). anders, es kann das Ergebnis der Aufgabenausführung zurückgeben, schauen Sie sich die Implementierung der Methode submit() an, Sie werden feststellen, dass es tatsächlich die Methode execute() aufruft, aber es verwendet die Future, um das Ergebnis der Aufgabenausführung zu erhalten (Future related Inhalt wird im nächsten Artikel beschrieben).

shutdown() und shutdownNow() werden verwendet, um den Thread-Pool herunterzufahren.

Es gibt viele andere Möglichkeiten:

Zum Beispiel: getQueue(), getPoolSize(), getActiveCount(), getCompletedTaskCount() und andere Methoden, um Eigenschaften zu erhalten, die sich auf Thread-Pools beziehen. Interessierte Freunde können die API selbst überprüfen.

Zweitens eingehende Analyse des Prinzips der Thread-Pool-Implementierung

Im vorigen Abschnitt haben wir ThreadPoolExecutor aus Makroperspektive vorgestellt. Analysieren wir nun das konkrete Implementierungsprinzip des Threadpools eingehend und erläutern es unter folgenden Aspekten:

1. Thread-Pool-Status

Eine flüchtige Variable wird in ThreadPoolExecutor definiert, und mehrere statische Endvariablen werden definiert, um die verschiedenen Zustände des Thread-Pools darzustellen:

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState stellt den Status des aktuellen Thread-Pools dar, der eine flüchtige Variable ist, die verwendet wird, um die Sichtbarkeit zwischen Threads sicherzustellen;

Die folgenden statischen Endvariablen repräsentieren mehrere mögliche Werte von runState.

Wenn der Thread-Pool erstellt wird, befindet sich der Thread-Pool anfänglich im Zustand RUNNING;

Wenn die Methode shutdown() aufgerufen wird, befindet sich der Thread-Pool im Zustand SHUTDOWN. Zu diesem Zeitpunkt kann der Thread-Pool keine neuen Tasks annehmen und wartet darauf, dass alle Tasks ausgeführt werden;

Wenn die Methode shutdownNow() aufgerufen wird, befindet sich der Thread-Pool im Zustand STOP.Zu diesem Zeitpunkt kann der Thread-Pool keine neuen Tasks annehmen und versucht, die ausgeführten Tasks zu beenden;

Wenn sich der Thread-Pool im Zustand SHUTDOWN oder STOP befindet und alle Worker-Threads zerstört wurden, die Task-Cache-Warteschlange geleert oder die Ausführung beendet wurde, wird der Thread-Pool in den Zustand TERMINATED versetzt.

2. Ausführung von Aufgaben

Bevor wir den gesamten Prozess der Übermittlung einer Aufgabe an den Thread-Pool bis zum Abschluss der Aufgabenausführung verstehen, werfen wir einen Blick auf einige andere wichtige Elementvariablen in der ThreadPoolExecutor-Klasse:

private final BlockingQueue<Runnable> workQueue;              //任务缓存队列,用来存放等待执行的任务
private final ReentrantLock mainLock = new ReentrantLock();   //线程池的主要状态锁,对线程池状态(比如线程池大小
                                                              //、runState等)的改变都要使用这个锁
private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集
 
private volatile long  keepAliveTime;    //线程存货时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
private volatile int   corePoolSize;     //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数
 
private volatile int   poolSize;       //线程池中当前的线程数
 
private volatile RejectedExecutionHandler handler; //任务拒绝策略
 
private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程
 
private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数
 
private long completedTaskCount;   //用来记录已经执行完毕的任务个数

Die Rolle jeder Variablen wurde markiert, um hier den Schwerpunkt auf die Erläuterung der drei Variablen corePoolSize, maximumPoolSize, largePoolSize zu legen.

corePoolSize wird an vielen Stellen in die Größe des Core-Pools übersetzt, nach meinem Verständnis handelt es sich tatsächlich um die Größe des Thread-Pools. Nehmen Sie ein einfaches Beispiel:

Wenn es eine Fabrik gibt, gibt es 10 Arbeiter in der Fabrik, und jeder Arbeiter kann nur eine Aufgabe gleichzeitig erledigen.

Solange also einer der 10 Arbeiter untätig ist, wird die Aufgabe dem untätigen Arbeiter zugewiesen;

Wenn 10 Arbeiter Aufgaben zu erledigen haben und es noch eine Aufgabe gibt, wird die Aufgabe in die Warteschlange gestellt und gewartet;

Wenn die Zahl der neuen Aufgaben viel schneller wächst als die Geschwindigkeit, mit der die Arbeiter Aufgaben erledigen, möchte der Fabrikaufseher möglicherweise Abhilfemaßnahmen ergreifen, wie z. B. die Einstellung von 4 Zeitarbeitern;

Dann werden die Aufgaben auch diesen 4 Aushilfen zugewiesen;

Wenn die Geschwindigkeit von 14 Arbeitern immer noch nicht ausreicht, kann der Fabrikaufseher erwägen, neue Aufgaben nicht anzunehmen oder einige frühere Aufgaben aufzugeben.

Wenn einige der 14 Arbeiter frei sind und die Wachstumsrate neuer Aufgaben relativ langsam ist, kann es sein, dass der Betriebsleiter erwägt, 4 Zeitarbeiter zu kündigen und nur die ursprünglichen 10 Arbeiter zu behalten, schließlich kostet es Geld, zusätzliche Arbeiter einzustellen.

Die corePoolSize in diesem Beispiel ist 10 und die maximumPoolSize ist 14 (10+4).

Mit anderen Worten, corePoolSize ist die Größe des Threadpools, und maximumPoolSize ist meiner Meinung nach eine Abhilfe für den Threadpool, also eine Abhilfe, wenn die Menge an Tasks plötzlich zu groß wird.

Zum leichteren Verständnis wird die corePoolSize jedoch weiter unten in diesem Artikel in die Core-Pool-Größe übersetzt.

largePoolSize ist nur eine für die Aufzeichnung verwendete Variable, die verwendet wird, um die größte Anzahl von Threads aufzuzeichnen, die jemals im Thread-Pool vorhanden waren, und hat nichts mit der Kapazität des Thread-Pools zu tun.

Lassen Sie uns nun zum Punkt kommen und sehen, welchen Prozess die Aufgabe von der Einreichung bis zur endgültigen Ausführung durchlaufen hat.

In der Klasse ThreadPoolExecutor stellt die Methode execute() die Kernmethode für die Aufgabenübermittlung dar. Obwohl Aufgaben auch über die Übermittlung übermittelt werden können, wird die Methode execute() tatsächlich in der Übermittlungsmethode aufgerufen, sodass wir nur die Ausführung untersuchen müssen () Methode Das Realisierungsprinzip kann sein:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        if (runState == RUNNING && workQueue.offer(command)) {
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
}

Der obige Code scheint nicht so einfach zu verstehen zu sein, lassen Sie uns ihn nacheinander erklären:

Bestimmen Sie zuerst, ob der übermittelte Aufgabenbefehl null ist.Wenn er null ist, wird eine Nullzeiger-Ausnahme ausgelöst;

Dann gibt es diesen Satz, der gut verstanden werden sollte:

if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

Da es sich um den bedingten OR-Operator handelt, wird die erste Hälfte des Werts zuerst berechnet. Wenn die aktuelle Anzahl von Threads im Thread-Pool nicht kleiner als die Core-Pool-Größe ist, wird direkt der folgende if-Anweisungsblock eingegeben.

Wenn die aktuelle Anzahl von Threads im Thread-Pool kleiner als die Größe des Core-Pools ist, dann führen Sie die zweite Hälfte aus, dh führen Sie aus

addIfUnderCorePoolSize(command)

Wenn die Methode addIfUnderCorePoolSize nach der Ausführung der Methode false zurückgibt, fahren Sie mit der Ausführung des folgenden if-Anweisungsblocks fort, da sonst die gesamte Methode direkt ausgeführt wird.

Wenn die addIfUnderCorePoolSize-Methode nach der Ausführung falsch zurückgibt, dann beurteilen Sie:

if (runState == RUNNING && workQueue.offer(command))

Wenn sich der aktuelle Thread-Pool im Zustand RUNNING befindet, die Aufgabe in die Task-Cache-Warteschlange stellen; wenn sich der aktuelle Thread-Pool nicht im Status RUNNING befindet oder die Aufgabe nicht in die Cache-Warteschlange gestellt werden kann, führen Sie Folgendes aus:

addIfUnderMaximumPoolSize(command)

Wenn die Ausführung der addIfUnderMaximumPoolSize-Methode fehlschlägt, führen Sie die respond()-Methode zur Aufgabenablehnung aus.

Zurück nach vorne:

if (runState == RUNNING && workQueue.offer(command))

Die Ausführung dieses Satzes, wenn sich der aktuelle Thread-Pool im Zustand RUNNING befindet und die Aufgabe erfolgreich in die Task-Cache-Warteschlange gestellt wurde, beurteilt weiterhin:

if (runState != RUNNING || poolSize == 0)

Diese Beurteilung ist eine Notfallmaßnahme, um zu verhindern, dass andere Threads plötzlich die Methode shutdown oder shutdownNow aufrufen, um den Thread-Pool zu schließen, wenn die Aufgabe der Aufgaben-Cache-Warteschlange hinzugefügt wird. Wenn ja ausführen:

ensureQueuedTaskHandled(command)

Die Notfallverarbeitung soll, wie der Name schon sagt, sicherstellen, dass Aufgaben, die der Aufgaben-Cache-Warteschlange hinzugefügt wurden, verarbeitet werden.

Schauen wir uns die Implementierung von zwei Schlüsselmethoden an: addIfUnderCorePoolSize und addIfUnderMaximumPoolSize:

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < corePoolSize && runState == RUNNING)
            t = addThread(firstTask);        //创建线程去执行firstTask任务   
        } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

Dies ist die spezifische Implementierung der Methode addIfUnderCorePoolSize.Aus dem Namen ist ersichtlich, dass ihre Absicht darin besteht, die Methode auszuführen,wenn sie kleiner als die Kerngröße ist. Schauen wir uns die spezifische Implementierung an.Zunächst wird die Sperre erhalten, da an dieser Stelle der Status des Thread-Pools geändert wird.Beurteilen Sie zunächst, ob die Anzahl der Threads im aktuellen Thread-Pool kleiner ist als die Größe des Core-Pools durch die if-Anweisung. Einige Freunde haben vielleicht Zweifel: before execute Wurde es nicht in der ()-Methode beurteilt? Die addIfUnderCorePoolSize-Methode wird nur ausgeführt, wenn die aktuelle Anzahl von Threads im Thread-Pool kleiner als die Core-Pool-Größe ist. Warum doch dieser Ort weiter zu urteilen? Der Grund ist sehr einfach. Es gibt keine Sperre im vorherigen Beurteilungsprozess. Daher kann die poolSize kleiner sein als die corePoolSize, wenn die Ausführungsmethode beurteilt wird. Nach der Beurteilung übermitteln andere Threads Aufgaben an den Thread-Pool, was die verursachen kann poolSize darf nicht kleiner sein als die corePoolSize. , also müssen Sie an dieser Stelle weiter urteilen. Dann wird beurteilt, ob der Status des Thread-Pools RUNNING ist, der Grund ist ganz einfach, denn es ist möglich, dass die Methode shutdown oder shutdownNow in anderen Threads aufgerufen wird. dann ausführen

t = addThread(firstTask);

Auch diese Methode ist sehr kritisch: Der übergebene Parameter ist die übergebene Aufgabe und der Rückgabewert ist vom Typ Thread. Beurteilen Sie dann, ob t leer ist oder nicht.Wenn es leer ist, zeigt es an, dass die Thread-Erstellung fehlgeschlagen ist (d. h. poolSize>=corePoolSize oder runState ist nicht gleich RUNNING), andernfalls wird die Methode t.start() aufgerufen starte den Faden.

Werfen wir einen Blick auf die Implementierung der Methode addThread:

private Thread addThread(Runnable firstTask) {
    Worker w = new Worker(firstTask);
    Thread t = threadFactory.newThread(w);  //创建一个线程,执行任务   
    if (t != null) {
        w.thread = t;            //将创建的线程的引用赋值为w的成员变量       
        workers.add(w);
        int nt = ++poolSize;     //当前线程数加1       
        if (nt > largestPoolSize)
            largestPoolSize = nt;
    }
    return t;
}

Erstellen Sie in der addThread-Methode zuerst ein Worker-Objekt mit der übermittelten Aufgabe, rufen Sie dann die Thread-Factory threadFactory auf, um einen neuen Thread t zu erstellen, weisen Sie dann die Referenz von Thread t der Member-Variablen thread des Worker-Objekts zu und übergeben Sie dann Worker. add (w) Fügt das Worker-Objekt zum Workingset hinzu.

Werfen wir einen Blick auf die Implementierung der Worker-Klasse:

private final class Worker implements Runnable {
    private final ReentrantLock runLock = new ReentrantLock();
    private Runnable firstTask;
    volatile long completedTasks;
    Thread thread;
    Worker(Runnable firstTask) {
        this.firstTask = firstTask;
    }
    boolean isActive() {
        return runLock.isLocked();
    }
    void interruptIfIdle() {
        final ReentrantLock runLock = this.runLock;
        if (runLock.tryLock()) {
            try {
        if (thread != Thread.currentThread())
        thread.interrupt();
            } finally {
                runLock.unlock();
            }
        }
    }
    void interruptNow() {
        thread.interrupt();
    }
 
    private void runTask(Runnable task) {
        final ReentrantLock runLock = this.runLock;
        runLock.lock();
        try {
            if (runState < STOP &&
                Thread.interrupted() &&
                runState >= STOP)
            boolean ran = false;
            beforeExecute(thread, task);   //beforeExecute方法是ThreadPoolExecutor类的一个方法,没有具体实现,用户可以根据
            //自己需要重载这个方法和后面的afterExecute方法来进行一些统计信息,比如某个任务的执行时间等           
            try {
                task.run();
                ran = true;
                afterExecute(task, null);
                ++completedTasks;
            } catch (RuntimeException ex) {
                if (!ran)
                    afterExecute(task, ex);
                throw ex;
            }
        } finally {
            runLock.unlock();
        }
    }
 
    public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);   //当任务队列中没有任务时,进行清理工作       
        }
    }
}

Es implementiert tatsächlich die Runnable-Schnittstelle, daher ist die Wirkung des obigen Threads t = threadFactory.newThread(w); im Grunde dieselbe wie die Wirkung des folgenden Satzes:

Thread t = new Thread(w);

Dies entspricht dem Übergeben einer ausführbaren Aufgabe und dem Ausführen dieser ausführbaren Aufgabe in Thread t.

Da Worker die Runnable-Schnittstelle implementiert, ist die natürliche Kernmethode die Methode run():

public void run() {
    try {
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
            runTask(task);
            task = null;
        }
    } finally {
        workerDone(this);
    }
}

Aus der Implementierung der run-Methode ist ersichtlich, dass sie zuerst die über den Konstruktor übergebene Aufgabe firstTask ausführt.Nachdem sie runTask() aufgerufen hat, um die firstTask auszuführen, fährt sie damit fort, neue Aufgaben zur Ausführung durch getTask() in der While-Schleife abzurufen Wo kann man es bekommen? Natürlich wird es aus der Task-Cache-Warteschlange genommen. getTask ist eine Methode in der ThreadPoolExecutor-Klasse, keine Methode in der Worker-Klasse. Das Folgende ist die Implementierung der getTask-Methode:

Runnable getTask() {
    for (;;) {
        try {
            int state = runState;
            if (state > SHUTDOWN)
                return null;
            Runnable r;
            if (state == SHUTDOWN)  // Help drain queue
                r = workQueue.poll();
            else if (poolSize > corePoolSize || allowCoreThreadTimeOut) //如果线程数大于核心池大小或者允许为核心池线程设置空闲时间,
                //则通过poll取任务,若等待一定的时间取不到任务,则返回null
                r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
            else
                r = workQueue.take();
            if (r != null)
                return r;
            if (workerCanExit()) {    //如果没取到任务,即r为null,则判断当前的worker是否可以退出
                if (runState >= SHUTDOWN) // Wake up others
                    interruptIdleWorkers();   //中断处于空闲状态的worker
                return null;
            }
            // Else retry
        } catch (InterruptedException ie) {
            // On interruption, re-check runState
        }
    }
}

Beurteilen Sie in getTask zuerst den aktuellen Status des Thread-Pools, und wenn runState größer als SHUTDOWN ist (dh STOP oder TERMINATED), geben Sie direkt null zurück.

Wenn runState SHUTDOWN oder RUNNING ist, wird die Task aus der Task-Cache-Warteschlange abgerufen.

Wenn die Anzahl der Threads im aktuellen Thread-Pool größer als die Core-Pool-Größe corePoolSize ist oder die Leerlauf-Überlebenszeit für die Threads im Core-Pool festgelegt werden darf, rufen Sie poll(time, timeUnit) auf, um die Aufgabe, diese Methode, abzurufen wartet eine bestimmte Zeit, wenn die Aufgabe nicht abgeholt werden kann, gibt null zurück.

Beurteilen Sie dann, ob die abgerufene Aufgabe r null ist, und wenn sie null ist, beurteilen Sie, ob der aktuelle Worker beenden kann, indem Sie die Methode workerCanExit() aufrufen. Schauen wir uns die Implementierung von workerCanExit() an:

private boolean workerCanExit() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    boolean canExit;
    //如果runState大于等于STOP,或者任务缓存队列为空了
    //或者  允许为核心池线程设置空闲存活时间并且线程池中的线程数目大于1
    try {
        canExit = runState >= STOP ||
            workQueue.isEmpty() ||
            (allowCoreThreadTimeOut &&
             poolSize > Math.max(1, corePoolSize));
    } finally {
        mainLock.unlock();
    }
    return canExit;
}

Das heißt, wenn sich der Thread-Pool im STOP-Zustand befindet oder die Task-Warteschlange leer ist oder die Idle Survival Time für den Core-Pool-Thread eingestellt werden darf und die Anzahl der Threads größer als 1 ist, ist der Worker leer ausreisen dürfen. Wenn der Worker verlassen werden darf, rufen Sie interruptIdleWorkers() auf, um den inaktiven Worker zu unterbrechen. Sehen wir uns die Implementierung von interruptIdleWorkers() an:

void interruptIdleWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)  //实际上调用的是worker的interruptIfIdle()方法
            w.interruptIfIdle();
    } finally {
        mainLock.unlock();
    }
}

Wie aus der Implementierung ersichtlich ist, ruft sie tatsächlich die Methode interruptIfIdle() des Workers in der Methode interruptIfIdle() des Workers auf:

void interruptIfIdle() {
    final ReentrantLock runLock = this.runLock;
    if (runLock.tryLock()) {    //注意这里,是调用tryLock()来获取锁的,因为如果当前worker正在执行任务,锁已经被获取了,是无法获取到锁的
                                //如果成功获取了锁,说明当前worker处于空闲状态
        try {
    if (thread != Thread.currentThread())  
    thread.interrupt();
        } finally {
            runLock.unlock();
        }
    }
}

Hier gibt es eine sehr ausgeklügelte Entwurfsmethode.Wenn wir den Thread-Pool entwerfen, kann es einen Task-Dispatching-Thread geben.Wenn festgestellt wird, dass ein Thread im Leerlauf ist, wird ein Task aus der Task-Cache-Warteschlangeentnommen und an den im Leerlauf befindlichen Thread übergeben die Ausführung. Diese Methode wird hier jedoch nicht übernommen, da sie zusätzlich den Task-Dispatching-Thread verwaltet, was die Schwierigkeit und Komplexität unsichtbar erhöht.Hier wird der Thread, der die Task ausgeführt hat, direkt aufgefordert, in die Task-Cache-Warteschlange zu gehen, um die zu holen Aufgabe zur Ausführung. .

Schauen wir uns die Implementierung der Methode addIfUnderMaximumPoolSize an. Die Implementierungsidee dieser Methode ist der der Methode addIfUnderCorePoolSize sehr ähnlich. Der einzige Unterschied besteht darin, dass die Methode addIfUnderMaximumPoolSize darin besteht, dass die Anzahl der Threads im Thread-Pool den Core-Pool erreicht Größe und das Hinzufügen von Aufgaben zur Aufgabenwarteschlange schlägt fehl. Wird in folgenden Fällen ausgeführt:

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (poolSize < maximumPoolSize && runState == RUNNING)
            t = addThread(firstTask);
    } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}

Siehe nein, tatsächlich ist es im Grunde dasselbe wie die Implementierung der addIfUnderCorePoolSize-Methode, außer dass poolSize < maximumPoolSize in der Beurteilungsbedingung der if-Anweisung anders ist.

An diesem Punkt sollten die meisten meiner Freunde ein grundlegendes Verständnis des gesamten Prozesses von der Übermittlung der Aufgabe an den Thread-Pool bis zur Ausführung haben.

1) Machen Sie sich zunächst die Bedeutung von corePoolSize und maximumPoolSize klar;

2) Zweitens müssen Sie wissen, für welche Rolle Worker verwendet wird;

3) Um die Verarbeitungsstrategie zu kennen, nachdem die Aufgabe an den Thread-Pool gesendet wurde, hier sind vier Hauptpunkte:

  • Wenn die Anzahl der Threads im aktuellen Thread-Pool kleiner als corePoolSize ist, wird jedes Mal, wenn eine Aufgabe kommt, ein Thread erstellt, um die Aufgabe auszuführen;
  • Wenn die Anzahl der Threads im aktuellen Thread-Pool >= corePoolSize ist, wird jedes Mal, wenn eine Aufgabe kommt, versucht, sie der Aufgaben-Cache-Warteschlange hinzuzufügen. Wenn das Hinzufügen erfolgreich ist, wartet die Aufgabe darauf, dass ein inaktiver Thread sie übernimmt zur Ausführung aus; wenn das Hinzufügen fehlschlägt (im Allgemeinen ist die Task-Cache-Warteschlange voll), wird versucht, einen neuen Thread zu erstellen, um die Task auszuführen;
  • Wenn die Anzahl der Threads im aktuellen Thread-Pool die maximale PoolSize erreicht, wird die Task-Ablehnungsrichtlinie für die Verarbeitung übernommen;
  • Wenn die Anzahl der Threads im Thread-Pool größer als corePoolSize ist und die Leerlaufzeit eines Threads keepAliveTime überschreitet, wird der Thread beendet, bis die Anzahl der Threads im Thread-Pool nicht größer als corePoolSize ist, sofern dies festgelegt werden darf die Überlebenszeit für die Threads im Core-Pool, dann überschreitet der Core-Pool den Thread in der Leerlaufzeit keepAliveTime, der Thread wird ebenfalls beendet.

3. Thread-Initialisierung im Thread-Pool

Nachdem der Thread-Pool erstellt wurde, befinden sich standardmäßig keine Threads im Thread-Pool, und Threads werden nur erstellt, nachdem Aufgaben übermittelt wurden.

Wenn Sie unmittelbar nach der Erstellung des Thread-Pools einen Thread erstellen müssen, können Sie dies in der Praxis auf zwei Arten tun:

  • prestartCoreThread(): Initialisiert einen Core-Thread;
  • prestartAllCoreThreads(): Initialisiert alle Core-Threads

Das Folgende ist die Implementierung dieser beiden Methoden:

public boolean prestartCoreThread() {
    return addIfUnderCorePoolSize(null); //注意传进去的参数是null
}
 
public int prestartAllCoreThreads() {
    int n = 0;
    while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
        ++n;
    return n;
}

Beachten Sie, dass der oben übergebene Parameter null ist.Wenn gemäß der Analyse in Abschnitt 2 der übergebene Parameter null ist, wird der endgültige Ausführungs-Thread in der getTask-Methode blockiert.

r = workQueue.take();

Das heißt, auf eine Aufgabe in der Aufgabenwarteschlange warten.

4. Task-Cache-Warteschlange und Warteschlangenstrategie

Wir haben die Aufgaben-Cache-Warteschlange schon oft erwähnt, d. h. workQueue, die zum Speichern von Aufgaben verwendet wird, die auf ihre Ausführung warten.

Der Typ von workQueue ist BlockingQueue<Runnable>, der normalerweise die folgenden drei Typen annehmen kann:

1) ArrayBlockingQueue: eine Array-basierte First-In-First-Out-Warteschlange, deren Größe angegeben werden muss, wenn die Warteschlange erstellt wird;

2) LinkedBlockingQueue: Eine First-in-First-out-Warteschlange basierend auf einer verknüpften Liste.Wenn die Warteschlangengröße bei ihrer Erstellung nicht angegeben wird, ist der Standardwert Integer.MAX_VALUE;

3) synchroneWarteschlange: Diese Warteschlange ist etwas Besonderes, sie speichert die übermittelten Aufgaben nicht, sondern erstellt direkt einen neuen Thread, um die neue Aufgabe auszuführen.

5. Richtlinie zur Ablehnung von Aufgaben

Wenn die Task-Cache-Warteschlange des Thread-Pools voll ist und die Anzahl der Threads im Thread-Pool die maximale PoolSize erreicht, wird, wenn noch Tasks kommen, die Task-Rejection-Strategie angewendet.Es gibt normalerweise die folgenden vier Strategien:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

6. Schließen des Thread-Pools

ThreadPoolExecutor bietet zwei Methoden zum Herunterfahren des Thread-Pools, shutdown() und shutdownNow(), wobei:

  • shutdown(): Der Thread-Pool wird nicht sofort beendet, sondern beendet, nachdem alle Tasks in der Task-Cache-Warteschlange ausgeführt wurden, aber es werden keine neuen Tasks akzeptiert.
  • shutdownNow(): Beenden Sie sofort den Thread-Pool und versuchen Sie, die ausgeführte Aufgabe zu unterbrechen, löschen Sie die Aufgaben-Cache-Warteschlange und geben Sie die nicht ausgeführte Aufgabe zurück

7. Dynamische Anpassung der Thread-Pool-Kapazität

ThreadPoolExecutor stellt Methoden bereit, um die Größe des Thread-Pools dynamisch anzupassen: setCorePoolSize() und setMaximumPoolSize(),

  • setCorePoolSize: Legen Sie die Core-Pool-Größe fest
  • setMaximumPoolSize: Legen Sie die maximale Anzahl von Threads fest, die der Thread-Pool erstellen kann

Wenn sich die obigen Parameter von klein auf groß ändern, führt ThreadPoolExecutor eine Thread-Zuweisung durch und kann sofort neue Threads erstellen, um Aufgaben auszuführen.

3. Anwendungsbeispiel

Zuvor haben wir das Implementierungsprinzip des Thread-Pools besprochen. In diesem Abschnitt werfen wir einen Blick auf seine spezifische Verwendung:

public class Test {
     public static void main(String[] args) {   
         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                 new ArrayBlockingQueue<Runnable>(5));
          
         for(int i=0;i<15;i++){
             MyTask myTask = new MyTask(i);
             executor.execute(myTask);
             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
             executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
         }
         executor.shutdown();
     }
}
 
 
class MyTask implements Runnable {
    private int taskNum;
     
    public MyTask(int num) {
        this.taskNum = num;
    }
     
    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕");
    }
}

Ergebnisse der:

正在执行task 0
线程池中线程数目:1,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
线程池中线程数目:2,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 1
线程池中线程数目:3,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 2
线程池中线程数目:4,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 3
线程池中线程数目:5,队列中等待执行的任务数目:0,已执行玩别的任务数目:0
正在执行task 4
线程池中线程数目:5,队列中等待执行的任务数目:1,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:2,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:3,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:4,已执行玩别的任务数目:0
线程池中线程数目:5,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
线程池中线程数目:6,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 10
线程池中线程数目:7,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 11
线程池中线程数目:8,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 12
线程池中线程数目:9,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 13
线程池中线程数目:10,队列中等待执行的任务数目:5,已执行玩别的任务数目:0
正在执行task 14
task 3执行完毕
task 0执行完毕
task 2执行完毕
task 1执行完毕
正在执行task 8
正在执行task 7
正在执行task 6
正在执行task 5
task 4执行完毕
task 10执行完毕
task 11执行完毕
task 13执行完毕
task 12执行完毕
正在执行task 9
task 14执行完毕
task 8执行完毕
task 5执行完毕
task 7执行完毕
task 6执行完毕
task 9执行完毕

Aus den Ausführungsergebnissen ist ersichtlich, dass, wenn die Anzahl der Threads im Thread-Pool größer als 5 ist, die Aufgabe in die Aufgaben-Cache-Warteschlange gestellt wird, und wenn die Aufgaben-Cache-Warteschlange voll ist, ein neuer Thread erstellt wird. Wenn im obigen Programm die for-Schleife geändert wird, um 20 Aufgaben auszuführen, wird eine Ausnahme zur Aufgabenzurückweisung ausgelöst.

In der Java-Dokumentation empfehlen wir jedoch nicht, ThreadPoolExecutor direkt zu verwenden, sondern mehrere statische Methoden zu verwenden, die in der Executors-Klasse bereitgestellt werden, um einen Thread-Pool zu erstellen:

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池
Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

Das Folgende ist die spezifische Implementierung dieser drei statischen Methoden:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Von ihrer spezifischen Implementierung aus rufen sie tatsächlich ThreadPoolExecutor auf, aber die Parameter wurden konfiguriert.

Die von newFixedThreadPool erstellten Werte für corePoolSize und maximumPoolSize des Thread-Pools sind gleich und verwenden LinkedBlockingQueue;

newSingleThreadExecutor setzt sowohl corePoolSize als auch maximumPoolSize auf 1 und verwendet auch LinkedBlockingQueue;

newCachedThreadPool setzt corePoolSize auf 0, maximumPoolSize auf Integer.MAX_VALUE und verwendet SynchronousQueue, was bedeutet, dass ein Thread zur Ausführung erstellt wird, wenn eine Aufgabe kommt, und wenn der Thread länger als 60 Sekunden im Leerlauf ist, wird der Thread zerstört.

Wenn die drei von Executors bereitgestellten statischen Methoden die Anforderungen erfüllen können, versuchen Sie in der Praxis, die drei von ihm bereitgestellten Methoden zu verwenden, da es etwas mühsam ist, die Parameter von ThreadPoolExecutor selbst manuell zu konfigurieren, und entsprechend konfiguriert werden muss die Art und Anzahl der eigentlichen Aufgaben.

Wenn ThreadPoolExecutor die Anforderungen nicht erfüllt, können Sie außerdem die ThreadPoolExecutor-Klasse erben und neu schreiben.

Viertens, wie man die Größe des Thread-Pools vernünftig konfiguriert

Dieser Abschnitt behandelt ein wichtigeres Thema: wie man die Größe des Thread-Pools vernünftig konfiguriert, nur als Referenz.

Im Allgemeinen muss die Größe des Thread-Pools entsprechend der Art der Aufgabe konfiguriert werden:

Wenn es sich um eine CPU-intensive Aufgabe handelt, müssen Sie die CPU so weit wie möglich auslasten.Der Referenzwert kann auf  N CPU+1 gesetzt werden

Handelt es sich um eine IO-intensive Aufgabe, kann der Referenzwert auf 2* N CPU gesetzt werden

Dies ist natürlich nur ein Referenzwert, und die spezifischen Einstellungen müssen entsprechend der tatsächlichen Situation angepasst werden. Sie können beispielsweise zuerst die Thread-Pool-Größe auf den Referenzwert setzen und dann den Task-Betrieb, die Systemlast, und Ressourcennutzung, um entsprechende Anpassungen vorzunehmen.

Ich denke du magst

Origin blog.csdn.net/qq_41701956/article/details/123488240
Empfohlen
Rangfolge