In diesem Artikel werden hauptsächlich relevante Informationen vorgestellt, die die Analyse des Java-Thread-Pools und der Executor-Prinzipien im Detail erläutern, um jedem zu helfen, diesen Teil des Wissens zu verstehen
Detaillierte Analyse der Java-Thread-Pool- und Executor-Prinzipien
Thread-Pool-Funktion und Grundwissen
Bevor wir beginnen, besprechen wir „ Threads“ Das Konzept des „Pools“. „Thread-Pool“ ist, wie der Name schon sagt, ein Thread-Cache. Es handelt sich um eine Sammlung von einem oder mehreren Threads. Benutzer können die auszuführenden Aufgaben einfach in den Thread-Pool werfen, ohne sich zu sehr mit den Details der Ausführung zu beschäftigen. Was sind also die Funktionen des Thread-Pools? Oder was sind die Vorteile gegenüber der direkten Verwendung von Thread? Ich habe die folgenden Punkte kurz zusammengefasst:
Reduzieren Sie den durch Thread-Erstellung und -Zerstörung verursachten Verbrauch
Für die Implementierung von Java Thread habe ich im vorherigen Artikel geschrieben In diesem Blog analysiert. Java-Thread und Kernel-Thread sind 1:1 (Linux). Darüber hinaus verfügt Thread über viele Mitgliedsdaten in der Java-Schicht und der C++-Schicht, sodass Java Thread tatsächlich relativ umfangreich ist. Das Erstellen und Zerstören eines Java-Threads erfordert viel Arbeit sowohl vom Betriebssystem als auch von der JVM. Wenn der Java-Thread also zwischengespeichert wird, kann eine gewisse Effizienzverbesserung erzielt werden.
Bequemere und transparentere Implementierung der Computerressourcensteuerung
Um dies zu diskutieren, müssen Sie möglicherweise einige Beispiele nennen. Nehmen Sie als Beispiel den sehr berühmten Webserver Nginx, der für seine leistungsstarken Parallelitätsfähigkeiten und seinen geringen Ressourcenverbrauch bekannt ist. Um diese strengen Anforderungen zu erfüllen, begrenzt Nginx die Anzahl der Arbeitsthreads streng (Arbeitsthreads entsprechen im Allgemeinen der Anzahl der CPUs). Der Schwerpunkt dieses Entwurfs liegt auf der Reduzierung des durch Threadwechsel verursachten Leistungsverlusts. Diese Optimierungsmethode ist auch auf Java anwendbar. Wenn für jede Aufgabe ein neuer Thread erstellt wird, ist das Endergebnis, dass die Programmressourcen schwer zu kontrollieren sind (eine bestimmte Funktion füllt die CPU) und die Gesamtausführungsgeschwindigkeit relativ langsam ist. Der Java-Thread-Pool stellt FixedThreadPool bereit, mit dem Sie die maximale Anzahl von Threads steuern können.
Bei so viel oben erwähntem „Unsinn“ analysieren wir es anhand der Implementierung des Java-Thread-Pools! Der Thread-Pool von Java verfügt über mehrere Implementierungen:
cached ThreadPool
Das Merkmal des zwischengespeicherten Thread-Pools besteht darin, dass er vorherige Threads und neu übermittelte Aufgaben zwischenspeichert. Kann ausgeführt werden zwischengespeicherte Threads, wodurch der erste oben erwähnte Vorteil erreicht wird.
fester ThreadPool
Eine Funktion von CachedThreadPool besteht darin, dass ein neuer Thread erstellt wird, wenn kein inaktiver Thread zum Ausführen der neu übermittelten Aufgabe vorhanden ist. FixedThreadPool tut dies nicht. Es speichert die Aufgabe und wartet, bis ein inaktiver Thread vorhanden ist, bevor sie ausgeführt wird. Das heißt, der zweite oben erwähnte Vorteil wird erreicht.
geplanter ThreadPool
Das Merkmal des geplanten ThreadPools besteht darin, dass er eine Aufgabenplanung realisieren kann, z. B. die verzögerte Ausführung und die periodische Ausführung von Aufgaben.
Zusätzlich zu den oben genannten drei implementiert Java auch newWorkStealingPool, das auf dem Fork/Join-Framework basiert. Ich habe mich noch nicht damit befasst, also lasse ich es lieber. Bei der Parallelitätsunterstützung von Java wird Executor zum Packen verschiedener Thread-Pools verwendet. Der Name „Executor“ ist eigentlich nur ein Executor.
1. Implementierung von zwischengespeichertem ThreadPool und festem ThreadPool
Wie aus der vorherigen Beschreibung ersichtlich ist, sind diese beiden Thread-Pools sehr ähnlich. Dies ist tatsächlich der Fall. Wenn nicht, schauen wir uns ein praktisches Beispiel an:
ThreadPoolExecutor executor1 = (ThreadPoolExecutor)Executors.newCachedThreadPool();
ThreadPoolExecutor executor2 = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);
Dies sind zwei Methoden zum Erstellen neuer Thread-Pools. Sie sehen sehr ähnlich aus! Wenn Sie das nicht glauben, kann ich Ihnen nur die Wahrheit zeigen.
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
Ja, sie rufen denselben Konstruktor auf, nur mit leicht unterschiedlichen Parametern. Werfen wir also einen Blick auf die Bedeutung dieser Parameter und den Unterschied zwischen den beiden Parametersätzen. Zunächst müssen Sie noch den Konstruktor von ThreadPoolExecutor veröffentlichen.
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
Um frisch auszusehen, werde ich den Konstruktor einer anderen Ebene nicht veröffentlichen, und dieser Konstruktor ist nur eine einfache Zuweisung. Der Funktionsprototyp hier kann uns bereits viele Informationen geben. Ich muss sagen, dass die Benennung des JDK-Codes wirklich gut ist, genau wie Kommentare.
maximumPoolSize ist die maximale Anzahl von Threads im Thread-Pool. Dieser Wert ist Integer.MAX_VALUE, was im Grunde einer Unendlichkeit entspricht. Welche Art von Maschine kann Milliarden von Threads ausführen? ! Bei einem festen ThreadPool ist dieser Wert die vom Benutzer festgelegte Anzahl von Thread-Pools.
keepAliveTime und Unit bestimmen die Cache-Ablaufzeit des Threads; für den zwischengespeicherten ThreadPool beträgt die Cache-Ablaufzeit des Threads eine Minute. Mit anderen Worten, wenn ein Arbeitsthread eine Minute lang nichts zu tun hat, wird er widerrufen um Geld zu sparen. Die an den festen ThreadPool übergebene Zeit beträgt 0, was bedeutet, dass der Arbeitsthread im festen ThreadPool niemals abläuft.
corePoolSize是线程池的最小线程数;对于cached ThreadPool,这个值为0,因为在完全没有任务的情况下,cached ThreadPool的确会成为“光杆司令”。至于fixed ThreadPool,这个fixed已经表明corePoolSize是等于线程总数的。
接下来,我们根据一个简单的使用例子,来看看一下cached ThreadPool的流程。
public class Task implements Callable<String> { private String name; public Task(String name) { this.name = name; } @Override public String call() throws Exception { System.out.printf("%s: Starting at : %s\n", this.name, new Date()); return "hello, world"; } public static void main(String[] args) { ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newCachedThreadPool(); Task task = new Task("test"); Future<String> result = executor.submit(task); try { System.out.printf("%s\n", result.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); System.out.printf("Main ends at : %s\n", new Date()); } }
首先,来看看executor.submit(task),这其实调用了ThreadPoolExecutor.execute(Runnable command)方法,这个方法的代码如下,整段代码的逻辑是这样的。首先检查线程池的线程数是否不够corePoolSize,如果不够就直接新建线程并把command添加进去;如果线程数已经够了或者添加失败(多个线程增加添加的情况),就尝试把command添加到队列中(workQueue.offer(command)),如果添加失败了,就reject掉cmd。大体的逻辑是这样的,这段代码有很多基于线程安全的设计,这里为了不跑题,就先忽略细节了。
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
到这里,看起来线程池实现的整体思路其实也没多么复杂。但是还有一个问题——一个普通的Thread在执行完自己的run方法后会自动退出。那么线程池是如何实现Worker线程不断的干活,甚至在没有任务的时候。其实答案很简单,就是Worker其实在跑大循环,Worker实际运行方法如下:
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); /***/ try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); /***/ } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
关键就在这个while的判断条件,对于需要cached线程的情况下,getTask()会阻塞起来,如果缓存的时间过期,就会返回一个null,然后Worker就退出了,也就结束了它的服役周期。而在有任务的情况下,Woker会把task拿出来,然后调用task.run()执行任务,并通过Future通知客户线程(即future.get()返回)。这样一个简单的线程池使用过程就完了。。。
当然,线程池的很多精髓知识——基于线程安全的设计,我都没有分析。有兴趣可以自己分析一下,也可以和我讨论。此外Scheduled ThreadPool这里也没有分析,它的要点其实是调度,主要是根据时间最小堆来驱动的。
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Prinzipien von Thread-Pool und Executor in Java. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!