newCachedThreadPool()
przeciw newFixedThreadPool()
Kiedy powinienem użyć jednego lub drugiego? Która strategia jest lepsza pod względem wykorzystania zasobów?
newCachedThreadPool()
przeciw newFixedThreadPool()
Kiedy powinienem użyć jednego lub drugiego? Która strategia jest lepsza pod względem wykorzystania zasobów?
Odpowiedzi:
Myślę, że dokumentacja dość dobrze wyjaśnia różnicę i użycie tych dwóch funkcji:
Tworzy pulę wątków, która ponownie wykorzystuje stałą liczbę wątków działających poza udostępnioną kolejką nieograniczoną. W dowolnym momencie co najwyżej nThreads będzie aktywnych zadań przetwarzania. Jeśli dodatkowe zadania zostaną przesłane, gdy wszystkie wątki będą aktywne, będą czekać w kolejce, aż wątek będzie dostępny. Jeśli jakikolwiek wątek zakończy się z powodu awarii podczas wykonywania przed zamknięciem, nowy wątek zajmie jego miejsce, jeśli będzie to konieczne do wykonania kolejnych zadań. Wątki w puli będą istniały do momentu jawnego zamknięcia.
Tworzy pulę wątków, która w razie potrzeby tworzy nowe wątki, ale będzie ponownie używać wcześniej utworzonych wątków, gdy będą dostępne. Te pule zwykle poprawiają wydajność programów, które wykonują wiele krótkotrwałych zadań asynchronicznych. Wywołania do wykonania spowodują ponowne użycie wcześniej utworzonych wątków, jeśli są dostępne. Jeśli żaden istniejący wątek nie jest dostępny, zostanie utworzony nowy wątek i dodany do puli. Wątki, które nie były używane przez sześćdziesiąt sekund, są przerywane i usuwane z pamięci podręcznej. W związku z tym pula, która pozostaje bezczynna wystarczająco długo, nie zużywa żadnych zasobów. Należy zauważyć, że pule o podobnych właściwościach, ale z różnymi szczegółami (na przykład parametrami limitu czasu) można tworzyć za pomocą konstruktorów ThreadPoolExecutor.
Jeśli chodzi o zasoby, newFixedThreadPool
wszystkie wątki będą działały, dopóki nie zostaną jawnie zakończone. W newCachedThreadPool
wątkach, które nie były używane przez sześćdziesiąt sekund, są przerywane i usuwane z pamięci podręcznej.
Biorąc to pod uwagę, zużycie zasobów będzie zależało w dużym stopniu od sytuacji. Na przykład, jeśli masz ogromną liczbę długo działających zadań, sugerowałbym rozszerzenie FixedThreadPool
. Jeśli chodzi o CachedThreadPool
, dokumentacja mówi, że „Te pule zazwyczaj poprawiają wydajność programów wykonujących wiele krótkotrwałych zadań asynchronicznych”.
newCachedThreadPool
może to spowodować poważne problemy, ponieważ pozostawiasz całą kontrolę thread pool
nad usługą i gdy usługa działa z innymi na tym samym hoście , co może spowodować awarię innych z powodu długiego oczekiwania na procesor. Więc myślę, że newFixedThreadPool
może być bezpieczniej w tego rodzaju scenariuszu. Również ten post wyjaśnia najważniejsze różnice między nimi.
Aby uzupełnić pozostałe odpowiedzi, chciałbym zacytować Effective Java, 2nd Edition, Joshua Bloch, rozdział 10, pozycja 68:
„Wybór usługi executora dla konkretnej aplikacji może być trudna. Jeśli piszesz mały program lub lekko obciążony serwer , używając Executors.new- CachedThreadPool jest ogólnie dobrym wyborem , ponieważ nie wymaga konfiguracji i generalnie„ nie właściwa rzecz." Jednak buforowana pula wątków nie jest dobrym wyborem dla mocno obciążonego serwera produkcyjnego !
W pamięci podręcznej puli wątków , złożone zadania nie są w kolejce , ale natychmiast przekazany do wątku do realizacji. Jeśli nie ma żadnych wątków, tworzony jest nowy . Jeśli serwer jest tak mocno obciążony, że wszystkie jego procesory są w pełni wykorzystane i przychodzi więcej zadań, zostanie utworzonych więcej wątków, co tylko pogorszy sprawę.
Dlatego na mocno obciążonym serwerze produkcyjnym znacznie lepiej jest używać Executors.newFixedThreadPool , który zapewnia pulę ze stałą liczbą wątków lub bezpośrednio używać klasy ThreadPoolExecutor, aby uzyskać maksymalną kontrolę. "
Jeśli spojrzysz na kod źródłowy , zobaczysz, że wywołują ThreadPoolExecutor. wewnętrznie i ustalając ich właściwości. Możesz utworzyć własny, aby mieć lepszą kontrolę nad swoimi wymaganiami.
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Jeśli nie martwisz się o nieograniczoną kolejkę zadań wywoływalnych / uruchamialnych , możesz użyć jednego z nich. Jak sugeruje Bruno, ja też wolę newFixedThreadPool
się newCachedThreadPool
nad tymi dwoma.
Ale ThreadPoolExecutor zapewnia bardziej elastyczne możliwości w porównaniu do jednej newFixedThreadPool
lubnewCachedThreadPool
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Zalety:
Masz pełną kontrolę nad rozmiarem BlockingQueue . Nie jest nieograniczony, w przeciwieństwie do poprzednich dwóch opcji. Nie otrzymam błędu braku pamięci z powodu ogromnego nagromadzenia oczekujących zadań wywoływalnych / uruchamialnych, gdy w systemie występują nieoczekiwane turbulencje.
Możesz wdrożyć niestandardowe zasady obsługi odrzucenia LUB skorzystać z jednej z zasad:
Domyślnie ThreadPoolExecutor.AbortPolicy
program obsługi zgłasza wyjątek RejectedExecutionException środowiska wykonawczego po odrzuceniu.
W ThreadPoolExecutor.CallerRunsPolicy
programie wątek, który wywołuje wykonanie, sam uruchamia zadanie. Zapewnia to prosty mechanizm kontroli sprzężenia zwrotnego, który spowolni tempo składania nowych zadań.
W programie ThreadPoolExecutor.DiscardPolicy
zadanie, którego nie można wykonać, jest po prostu odrzucane.
W programie ThreadPoolExecutor.DiscardOldestPolicy
, jeśli moduł wykonawczy nie zostanie zamknięty, zadanie na początku kolejki roboczej jest porzucane, a następnie ponawiane jest wykonanie (co może ponownie zakończyć się niepowodzeniem, powodując powtórzenie).
Możesz zaimplementować niestandardową fabrykę wątków dla poniższych przypadków użycia:
Zgadza się, Executors.newCachedThreadPool()
nie jest to doskonały wybór dla kodu serwera, który obsługuje wielu klientów i jednoczesne żądania.
Czemu? Zasadniczo są z nim dwa (powiązane) problemy:
Jest nieograniczony, co oznacza, że otwierasz każdemu drzwi do okaleczenia Twojej maszyny JVM, po prostu wstrzykując więcej pracy do usługi (atak DoS). Wątki zużywają niezauważalną ilość pamięci, a także zwiększają zużycie pamięci w oparciu o ich pracę w toku, więc dość łatwo jest w ten sposób obalić serwer (chyba że masz na miejscu inne wyłączniki).
Nieograniczony problem pogłębia fakt, że wykonawca jest poprzedzony przez, SynchronousQueue
co oznacza, że istnieje bezpośrednie przekazanie między zleceniodawcą a pulą wątków. Każde nowe zadanie spowoduje utworzenie nowego wątku, jeśli wszystkie istniejące wątki są zajęte. Jest to ogólnie zła strategia dla kodu serwera. Kiedy procesor się nasyca, istniejące zadania trwają dłużej. Jeszcze więcej zadań jest przesyłanych i tworzonych jest więcej wątków, więc wykonanie zadań trwa coraz dłużej. Gdy procesor jest nasycony, więcej wątków zdecydowanie nie jest tym, czego potrzebuje serwer.
Oto moje zalecenia:
Użyj puli wątków o stałym rozmiarze Executors.newFixedThreadPool lub ThreadPoolExecutor. z ustawioną maksymalną liczbą wątków;
ThreadPoolExecutor
Klasa jest realizacja baza dla wykonawców, które są zwracane z wielu Executors
metod fabrycznych. Podejdźmy więc do stałych i buforowanych pul wątków z platformyThreadPoolExecutor
perspektywy.
Główny konstruktor tej klasy wygląda następująco:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
corePoolSize
Określa minimalny rozmiar puli nici docelowej.Implementacja utrzymywałaby pulę o takim rozmiarze, nawet jeśli nie ma zadań do wykonania.
To maximumPoolSize
maksymalna liczba wątków, które mogą być jednocześnie aktywne.
Gdy pula wątków wzrośnie i stanie się większa niż corePoolSize
próg, moduł wykonujący może zakończyć bezczynne wątki i sięgnąć corePoolSize
ponownie. Jeśli allowCoreThreadTimeOut
ma wartość true, moduł wykonawczy może nawet zakończyć wątki puli podstawowej, jeśli były bezczynne dłużej niż keepAliveTime
próg.
Zatem najważniejsze jest to, że jeśli wątki pozostają bezczynne dłużej niż keepAliveTime
próg, mogą zostać zakończone, ponieważ nie ma na nie popytu.
Co się dzieje, gdy pojawia się nowe zadanie i wszystkie główne wątki są zajęte? Nowe zadania zostaną umieszczone w kolejce w tej BlockingQueue<Runnable>
instancji. Gdy wątek zostanie zwolniony, można przetworzyć jedno z tych zadań w kolejce.
Istnieją różne implementacje BlockingQueue
interfejsu w Javie, więc możemy zaimplementować różne podejścia do kolejkowania, takie jak:
Ograniczona kolejka : nowe zadania byłyby umieszczane w kolejce wewnątrz ograniczonej kolejki zadań.
Nieograniczona kolejka : nowe zadania byłyby umieszczane w kolejce w nieograniczonej kolejce zadań. Więc ta kolejka może rosnąć tak bardzo, jak pozwala na to rozmiar sterty.
Synchroniczne przekazywanie : Możemy również używać SynchronousQueue
do kolejkowania nowych zadań. W takim przypadku podczas kolejkowania nowego zadania inny wątek musi już czekać na to zadanie.
Oto jak ThreadPoolExecutor
wykonuje nowe zadanie:
corePoolSize
uruchomionych jest mniej wątków niż liczba , próbuje rozpocząć nowy wątek z danym zadaniem jako pierwszym zadaniem.BlockingQueue#offer
metody. offer
Metoda nie będzie blokować gdy kolejka jest pełny i wraca natychmiast false
.offer
Zwrócifalse
), wówczas próbuje dodać nowy wątek do puli wątków z tym zadaniem jako pierwszym zadaniem.RejectedExecutionHandler
.Główna różnica między stałymi i buforowanymi pulami wątków sprowadza się do tych trzech czynników:
+ ----------- + ----------- + ------------------- + ----- ---------------------------- + | Typ puli | Rozmiar rdzenia | Maksymalny rozmiar | Strategia kolejkowania | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | Naprawiono | n (naprawiono) | n (naprawiono) | Nieograniczony „LinkedBlockingQueue” | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | Buforowany | 0 | Integer.MAX_VALUE | `SynchronousQueue` | + ----------- + ----------- + ------------------- + ----- ---------------------------- +
Excutors.newFixedThreadPool(n)
działa:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Jak widzisz:
OutOfMemoryError
.Kiedy powinienem użyć jednego lub drugiego? Która strategia jest lepsza pod względem wykorzystania zasobów?
Pula wątków o stałym rozmiarze wydaje się być dobrym kandydatem, gdy zamierzamy ograniczyć liczbę współbieżnych zadań do celów zarządzania zasobami .
Na przykład, jeśli zamierzamy używać modułu wykonawczego do obsługi żądań serwera WWW, stały moduł wykonawczy może rozsądniej obsługiwać serie żądań.
Aby uzyskać jeszcze lepsze zarządzanie zasobami, zdecydowanie zaleca się utworzenie niestandardowego ThreadPoolExecutor
z ograniczoną BlockingQueue<T>
implementacją w połączeniu z rozsądną implementacją RejectedExecutionHandler
.
Oto jak to Executors.newCachedThreadPool()
działa:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Jak widzisz:
Integer.MAX_VALUE
. W praktyce pula wątków jest nieograniczona.SynchronousQueue
zawsze kończy się niepowodzeniem, gdy po drugiej stronie nie ma nikogo, kto by je zaakceptował!Kiedy powinienem użyć jednego lub drugiego? Która strategia jest lepsza pod względem wykorzystania zasobów?
Używaj go, gdy masz wiele przewidywalnych, krótkotrwałych zadań.
Należy używać newCachedThreadPool tylko wtedy, gdy masz krótkotrwałe zadania asynchroniczne, jak określono w Javadoc, jeśli przesyłasz zadania, których przetwarzanie zajmuje więcej czasu, w końcu utworzysz zbyt wiele wątków. Możesz osiągnąć 100% CPU, jeśli prześlesz długo działające zadania w szybszym tempie do newCachedThreadPool ( http://rashcoder.com/be-careful-while-using-executors-newcachedthreadpool/ ).
Robię kilka szybkich testów i mam następujące wyniki:
1) w przypadku korzystania z SynchronousQueue:
Gdy wątki osiągną maksymalny rozmiar, każda nowa praca zostanie odrzucona z wyjątkiem jak poniżej.
Wyjątek w wątku „main” java.util.concurrent.RejectedExecutionException: zadanie java.util.concurrent.FutureTask@3fee733d odrzucony z java.util.concurrent.ThreadPoolExecutor@5acf9800 [Uruchomione, rozmiar puli = 3, aktywne wątki = 3, zadania w kolejce = 0, ukończone zadania = 0]
w java.util.concurrent.ThreadPoolExecutor $ AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2047)
2) jeśli używasz LinkedBlockingQueue:
Wątki nigdy nie zwiększają się z rozmiaru minimalnego do maksymalnego, co oznacza, że pula wątków ma stały rozmiar jako rozmiar minimalny.