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?
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ę, żenewFixedThreadPool
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:
źródło
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.
źródło
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
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:
źródło
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;
źródło
ThreadPoolExecutor
Klasa jest realizacja baza dla wykonawców, które są zwracane z wieluExecutors
metod fabrycznych. Podejdźmy więc do stałych i buforowanych pul wątków z platformyThreadPoolExecutor
perspektywy.ThreadPoolExecutor
Główny konstruktor tej klasy wygląda następująco:
Rozmiar puli podstawowej
corePoolSize
Określa minimalny rozmiar puli nici docelowej.Implementacja utrzymywałaby pulę o takim rozmiarze, nawet jeśli nie ma zadań do wykonania.Maksymalny rozmiar basenu
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śliallowCoreThreadTimeOut
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.Kolejkowanie
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.Składanie prac
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 natychmiastfalse
.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:
Naprawiono pulę wątków
Oto jak to
Excutors.newFixedThreadPool(n)
działa:Jak widzisz:
OutOfMemoryError
.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
.Pula wątków w pamięci podręcznej
Oto jak to
Executors.newCachedThreadPool()
działa: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ł!Używaj go, gdy masz wiele przewidywalnych, krótkotrwałych zadań.
źródło
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/ ).
źródło
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.
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.
źródło