Executors.newCachedThreadPool () a Executors.newFixedThreadPool ()


Odpowiedzi:


202

Myślę, że dokumentacja dość dobrze wyjaśnia różnicę i użycie tych dwóch funkcji:

newFixedThreadPool

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.

newCachedThreadPool

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, newFixedThreadPoolwszystkie wątki będą działały, dopóki nie zostaną jawnie zakończone. W newCachedThreadPoolwą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”.


1
tak, przeszedłem przez dokumentację ... problem jest ... naprawionyThreadPool powoduje błąd braku pamięci @ 3 wątki ... gdzie jako cachedPool tworzy wewnętrznie tylko jeden wątek ... po zwiększeniu rozmiaru sterty otrzymuję to samo wydajność dla obu… czy jest coś, czego mi brakuje!
hakish

1
Czy udostępniasz fabrykę wątków do puli wątków? Domyślam się, że może to być przechowywanie stanu w wątkach, które nie są usuwane jako śmieci. Jeśli nie, być może Twój program działa tak blisko limitu wielkości sterty, że przy utworzeniu 3 wątków powoduje powstanie OutOfMemory. Ponadto, jeśli cachedPool wewnętrznie tworzy tylko jeden wątek, oznacza to, że Twoje zadania są zsynchronizowane.
bruno conde

@brunoconde Tak jak @Louis F. wskazuje, że newCachedThreadPoolmoże to spowodować poważne problemy, ponieważ pozostawiasz całą kontrolę thread poolnad 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 newFixedThreadPoolmoże być bezpieczniej w tego rodzaju scenariuszu. Również ten post wyjaśnia najważniejsze różnice między nimi.
Hearen

75

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ę. "


15

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>());
}

1
Dokładnie, buforowany moduł wykonawczy wątku z rozsądnym górnym limitem i powiedzmy, 5-10 minut bezczynnego zbierania jest idealny w większości przypadków.
Agoston Horvath

12

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ę newFixedThreadPoolsię newCachedThreadPoolnad tymi dwoma.

Ale ThreadPoolExecutor zapewnia bardziej elastyczne możliwości w porównaniu do jednej newFixedThreadPoollubnewCachedThreadPool

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

Zalety:

  1. 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.

  2. Możesz wdrożyć niestandardowe zasady obsługi odrzucenia LUB skorzystać z jednej z zasad:

    1. Domyślnie ThreadPoolExecutor.AbortPolicyprogram obsługi zgłasza wyjątek RejectedExecutionException środowiska wykonawczego po odrzuceniu.

    2. W ThreadPoolExecutor.CallerRunsPolicyprogramie 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ń.

    3. W programie ThreadPoolExecutor.DiscardPolicyzadanie, którego nie można wykonać, jest po prostu odrzucane.

    4. 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).

  3. Możesz zaimplementować niestandardową fabrykę wątków dla poniższych przypadków użycia:

    1. Aby ustawić bardziej opisową nazwę wątku
    2. Aby ustawić stan demona wątku
    3. Aby ustawić priorytet wątku

11

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:

  1. 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).

  2. Nieograniczony problem pogłębia fakt, że wykonawca jest poprzedzony przez, SynchronousQueueco 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;


6

ThreadPoolExecutorKlasa jest realizacja baza dla wykonawców, które są zwracane z wielu Executorsmetod 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:

public ThreadPoolExecutor(
                  int corePoolSize,
                  int maximumPoolSize,
                  long keepAliveTime,
                  TimeUnit unit,
                  BlockingQueue<Runnable> workQueue,
                  ThreadFactory threadFactory,
                  RejectedExecutionHandler handler
)

Rozmiar puli podstawowej

corePoolSizeOkreś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 maximumPoolSizemaksymalna liczba wątków, które mogą być jednocześnie aktywne.

Gdy pula wątków wzrośnie i stanie się większa niż corePoolSizepróg, moduł wykonujący może zakończyć bezczynne wątki i sięgnąć corePoolSizeponownie. Jeśli allowCoreThreadTimeOutma wartość true, moduł wykonawczy może nawet zakończyć wątki puli podstawowej, jeśli były bezczynne dłużej niż keepAliveTimepróg.

Zatem najważniejsze jest to, że jeśli wątki pozostają bezczynne dłużej niż keepAliveTimepró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 BlockingQueueinterfejsu w Javie, więc możemy zaimplementować różne podejścia do kolejkowania, takie jak:

  1. Ograniczona kolejka : nowe zadania byłyby umieszczane w kolejce wewnątrz ograniczonej kolejki zadań.

  2. 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.

  3. Synchroniczne przekazywanie : Możemy również używać SynchronousQueuedo kolejkowania nowych zadań. W takim przypadku podczas kolejkowania nowego zadania inny wątek musi już czekać na to zadanie.

Składanie prac

Oto jak ThreadPoolExecutorwykonuje nowe zadanie:

  1. Jeśli corePoolSizeuruchomionych jest mniej wątków niż liczba , próbuje rozpocząć nowy wątek z danym zadaniem jako pierwszym zadaniem.
  2. W przeciwnym razie próbuje umieścić nowe zadanie w kolejce przy użyciu BlockingQueue#offermetody. offerMetoda nie będzie blokować gdy kolejka jest pełny i wraca natychmiast false.
  3. Jeśli nie uda się umieścić w kolejce nowego zadania (tj. offerZwrócifalse ), wówczas próbuje dodać nowy wątek do puli wątków z tym zadaniem jako pierwszym zadaniem.
  4. Jeśli nie uda się dodać nowego wątku, wówczas executor jest zamknięty lub nasycony. Tak czy inaczej, nowe zadanie zostanie odrzucone przy użyciu podanego pliku RejectedExecutionHandler.

Główna różnica między stałymi i buforowanymi pulami wątków sprowadza się do tych trzech czynników:

  1. Rozmiar puli podstawowej
  2. Maksymalny rozmiar basenu
  3. Kolejkowanie
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Typ puli | Rozmiar rdzenia | Maksymalny rozmiar | Strategia kolejkowania |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Naprawiono | n (naprawiono) | n (naprawiono) | Nieograniczony „LinkedBlockingQueue” |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
| Buforowany | 0 | Integer.MAX_VALUE | `SynchronousQueue` |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +


Naprawiono pulę wątków


Oto jak to Excutors.newFixedThreadPool(n)działa:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

Jak widzisz:

  • Rozmiar puli wątków jest stały.
  • Jeśli jest duży popyt, nie wzrośnie.
  • Jeśli wątki będą nieaktywne przez dłuższy czas, nie będą się kurczyć.
  • Załóżmy, że wszystkie te wątki są zajęte pewnymi długotrwałymi zadaniami, a wskaźnik docierania jest nadal dość wysoki. Ponieważ executor używa nieograniczonej kolejki, może zużywać ogromną część sterty. Będąc niefortunnym, możemy doświadczyć 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 ThreadPoolExecutorz 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:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Jak widzisz:

  • Pula wątków może wzrosnąć od zera do Integer.MAX_VALUE. W praktyce pula wątków jest nieograniczona.
  • Jeśli jakikolwiek wątek jest bezczynny przez ponad 1 minutę, może zostać zakończony. Pula może się więc zmniejszyć, jeśli wątki pozostają zbyt długo bezczynne.
  • Jeśli wszystkie przydzielone wątki są zajęte, gdy pojawia się nowe zadanie, tworzy nowy wątek, ponieważ oferowanie nowego zadania SynchronousQueuezawsze 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ń.



1

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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.