Inny typ puli wątków jest buforowany. Te pule wątków są tak samo powszechne w użyciu, jak pule stałe.

Pula wątków buforuje wątki, stąd nazwa. Utrzymuje wątki aktywne (ale nieużywane) przez ograniczony czas w celu wykorzystania tych wątków do wykonywania nowych zadań. Ta pula wątków jest najlepiej używana, gdy mamy do wykonania jakąś rozsądną ilość lekkiej pracy.

Zrozumienie „pewnej rozsądnej kwoty” jest dość naciągane, ale musisz zrozumieć, że taka pula nie jest odpowiednia dla każdej liczby zadań. Na przykład, jeśli chcemy stworzyć milion zadań, z których każde zajmuje nawet bardzo mało czasu, nadal będziemy nieracjonalnie zużywać zasoby i obniżać wydajność. Również powinniśmy unikać takiej puli, gdy czas wykonania jest nieprzewidywalny, np. przy zadaniach I/O.

Pod maską konstruktor ThreadPoolExecutor jest wywoływany z następującymi parametrami:

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

Następujące wartości są przekazywane jako parametry do konstruktora:

Parametr Oznaczający
corePoolSize (ile wątków będzie gotowych (uruchomionych) po uruchomieniu usługi executora ) 0
MaximumPoolSize (Maksymalna liczba wątków, które może utworzyć usługa executora ) liczba całkowita.MAX_VALUE
keepAliveTime (Czas, przez który uwolniony wątek będzie żył, a następnie zostanie zabity, jeśli liczba wątków jest większa niż corePoolSize ) 60L
jednostka (jednostki tymczasowe) Jednostka czasu.SEKUNDY
workQueue (implementacja kolejki) nowa Kolejka Synchroniczna<Runnable>()

Możemy również przekazać do parametrów naszą własną implementację ThreadFactory .

Porozmawiajmy o kolejce synchronicznej

Podstawowa idea transferu synchronicznego jest dość prosta i jednocześnie sprzeczna z intuicją (nie wydaje się prawdziwa, gdy ocenia się ją na podstawie intuicji, zdrowego rozsądku czy emocji): element można umieścić w kolejce wtedy i tylko wtedy, gdy inny wątek otrzymuje ten element w tym samym czasie. Innymi słowy, kolejka synchroniczna nie może sama w sobie mieć zadań, ponieważ w momencie nadejścia nowego zadania wątek wykonujący już to zadanie podejmuje .

W momencie, gdy do kolejki wejdzie nowe zadanie, jeśli w puli jest wolny aktywny wątek, to przejmuje to zadanie, a jeśli wszystkie wątki są zajęte, tworzony jest nowy wątek.

Pula buforowana zaczyna się od zerowych wątków i może potencjalnie wzrosnąć do wątków Integer.MAX_VALUE . Prawie jedynym ograniczeniem buforowanej puli wątków są zasoby systemowe.

Aby zarządzać zasobami systemowymi, buforowane pule wątków usuwają wątki, które są bezczynne przez jedną minutę.

Zobaczmy, jak to działa w praktyce. Tworzymy klasę zadań, symulując żądanie użytkownika:

public class Task implements Runnable {
   int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
   }
}

W main tworzymy newCachedThreadPool , po czym dodajemy 3 zadania do wykonania. Tutaj wyświetlamy status naszej usługi (1) .

Następnie zatrzymujemy się na 30 sekund, po czym uruchamiamy kolejne zadanie do wykonania i wyświetlamy status (2) .

Następnie wstrzymujemy nasz główny wątek na 70 sekund, wyświetlamy status (3) i ponownie dodajemy 3 zadania do wykonania i ponownie drukujemy status (4) .

Przed wyświetleniem statusów w miejscach, w których status jest wyświetlany bezpośrednio po dodaniu zadania, ustawimy uśpienie na 1 sekundę dla rzeczywistego wyjścia.

ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)

        TimeUnit.SECONDS.sleep(30);

        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)

        TimeUnit.SECONDS.sleep(70);

            System.out.println(executorService);	//(3)

        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

Tak więc wynik wykonania:

Przetworzone żądanie użytkownika nr 0 w wątku puli-1-wątku-1
Przetworzone żądanie użytkownika nr 1 w wątku puli-1-wątku-2
Przetworzone żądanie użytkownika nr 2 w wątku puli-1-wątku-3
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[Uruchomiono, rozmiar puli = 3, aktywne wątki = 0, zadania w kolejce = 0, zadania zakończone = 3]
Przetworzone żądanie użytkownika nr 3 w wątku puli-1-wątku-2
(2) wątek java.util.concurrent. ThreadPoolExecutor@f6f4d33[Działa, rozmiar puli = 3, aktywne wątki = 0, zadania w kolejce = 0, zadania zakończone = 4] (3) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Pracuje,
rozmiar puli = 0, aktywne wątki = 0 zadania w kolejce = 0, zadania ukończone = 4]
Żądanie użytkownika nr 4 przetworzone w wątku puli-1-wątku-4
Przetworzone żądanie użytkownika nr 5 w wątku puli 1-wątku-5
Przetworzone żądanie użytkownika nr 6 w wątku puli-1-wątku-4
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Uruchomiono, rozmiar puli = 2, aktywne wątki = 0, zadania w kolejce = 0, zadania zakończone = 7]

Omówmy każdy z kroków:

Krok wyjaśnienie
1 (po 3 ukończonych zadaniach) Utworzono 3 wątki, na tych trzech wątkach wykonano 3 zadania.
W momencie wyjścia stanu wszystkie 3 zadania zostały zakończone, a wątki są gotowe do wykonania innych zadań.
2 (po przerwie 30 sekund i kolejnym zadaniu) Po 30 sekundach bezczynności wątki nadal żyją i czekają na zadania.
Kolejne zadanie jest dodawane i wykonywane na wątku z puli pozostałych aktywnych wątków.
W puli nie ma nowego wątku.
3 (po przerwie trwającej 70 sekund) Wątki zostały usunięte z puli.
Brak wątków gotowych do przyjęcia zadań.
4 (po wykonaniu 3 kolejnych zadań) Po pojawieniu się większej liczby zadań utworzono nowe wątki i tym razem tylko dwa wątki były w stanie przetworzyć 3 zadania.

W ten sposób zapoznaliśmy się z pracą innego rodzaju usługi z własną logiką kontroli przepływu.

Analogicznie do innych metod użytkowych klasy Executors , metoda newCachedThreadPool ma również swoją przeciążoną wersję, która jako parametr przyjmuje obiekt typu ThreadFactory .