Wie kann ich diese Einschränkung umgehen, ThreadPoolExecutor
wenn die Warteschlange begrenzt und voll sein muss, bevor weitere Threads gestartet werden?
Ich glaube, ich habe endlich eine etwas elegante (vielleicht etwas hackige) Lösung für diese Einschränkung gefunden ThreadPoolExecutor
. Es geht um erstreckt , LinkedBlockingQueue
es zu haben Rückkehr false
zu , queue.offer(...)
wenn es bereits einige Tasks in der Liste. Wenn die aktuellen Threads nicht mit den Aufgaben in der Warteschlange Schritt halten, fügt das TPE zusätzliche Threads hinzu. Befindet sich der Pool bereits bei maximaler Anzahl von Threads, RejectedExecutionHandler
wird der aufgerufen. Es ist der Handler, der das put(...)
in die Warteschlange stellt.
Es ist sicherlich seltsam, eine Warteschlange zu schreiben, offer(...)
die zurückkehren kann false
und put()
niemals blockiert, also ist das der Hack-Teil. Dies funktioniert jedoch gut mit der Verwendung der Warteschlange durch TPE, sodass ich kein Problem damit sehe.
Hier ist der Code:
// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
private static final long serialVersionUID = -6903933921423432194L;
@Override
public boolean offer(Runnable e) {
// Offer it to the queue if there is 0 items already queued, else
// return false so the TPE will add another thread. If we return false
// and max threads have been reached then the RejectedExecutionHandler
// will be called which will do the put into the queue.
if (size() == 0) {
return super.offer(e);
} else {
return false;
}
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// This does the actual put into the queue. Once the max threads
// have been reached, the tasks will then queue up.
executor.getQueue().put(r);
// we do this after the put() to stop race conditions
if (executor.isShutdown()) {
throw new RejectedExecutionException(
"Task " + r + " rejected from " + e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
Wenn ich mit diesem Mechanismus Aufgaben an die Warteschlange sende, wird der ThreadPoolExecutor
Wille:
- Skalieren Sie die Anzahl der Threads zunächst auf die Kerngröße (hier 1).
- Bieten Sie es der Warteschlange an. Wenn die Warteschlange leer ist, wird sie in die Warteschlange gestellt, damit sie von den vorhandenen Threads verarbeitet werden kann.
- Wenn die Warteschlange bereits 1 oder mehrere Elemente enthält,
offer(...)
wird false zurückgegeben.
- Wenn false zurückgegeben wird, skalieren Sie die Anzahl der Threads im Pool, bis sie die maximale Anzahl erreichen (hier 50).
- Wenn am Maximum, dann ruft es die
RejectedExecutionHandler
- Die
RejectedExecutionHandler
dann legt die Aufgabe in die Warteschlange durch den ersten verfügbaren Thread in FIFO - Reihenfolge verarbeitet werden.
Obwohl in meinem obigen Beispielcode die Warteschlange unbegrenzt ist, können Sie sie auch als begrenzte Warteschlange definieren. Wenn Sie beispielsweise eine Kapazität von 1000 hinzufügen LinkedBlockingQueue
, wird Folgendes ausgeführt:
- Skalieren Sie die Gewinde auf max
- Stellen Sie sich dann in die Warteschlange, bis 1000 Aufgaben erledigt sind
- Blockieren Sie dann den Anrufer, bis der Warteschlange Speicherplatz zur Verfügung steht.
Auch, wenn Sie verwenden benötigt offer(...)
in der
RejectedExecutionHandler
dann könnte man die Verwendung offer(E, long, TimeUnit)
Methode stattdessen mit Long.MAX_VALUE
als Timeout.
Warnung:
Wenn Sie erwarten, dass dem Executor nach dem Herunterfahren Aufgaben hinzugefügt werden , sollten Sie klüger sein RejectedExecutionException
, RejectedExecutionHandler
wenn Sie den Executor-Service heruntergefahren haben. Vielen Dank an @RaduToader für diesen Hinweis.
Bearbeiten:
Eine weitere Optimierung dieser Antwort könnte darin bestehen, das TPE zu fragen, ob Leerlauf-Threads vorhanden sind, und das Element nur dann in die Warteschlange zu stellen, wenn dies der Fall ist. Sie müssten dafür eine echte Klasse erstellen und eine ourQueue.setThreadPoolExecutor(tpe);
Methode hinzufügen .
Dann offer(...)
könnte Ihre Methode ungefähr so aussehen:
- Überprüfen Sie, ob die
tpe.getPoolSize() == tpe.getMaximumPoolSize()
in diesem Fall nur anrufen super.offer(...)
.
- Andernfalls
tpe.getPoolSize() > tpe.getActiveCount()
rufen Sie an, super.offer(...)
da es scheinbar inaktive Threads gibt.
- Andernfalls kehren Sie
false
zu einem anderen Thread zurück.
Vielleicht das:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Beachten Sie, dass die get-Methoden für TPE teuer sind, da sie auf volatile
Felder zugreifen oder (im Fall von getActiveCount()
) das TPE sperren und die Thread-Liste durchlaufen. Außerdem gibt es hier Rennbedingungen, die dazu führen können, dass eine Aufgabe nicht ordnungsgemäß in die Warteschlange gestellt oder ein anderer Thread gegabelt wird, wenn ein Leerlauf-Thread vorhanden ist.