Pule wątków - podstawy

Andrzej Galiński · 2021-04-18

Cel: Napisać minimalną aplikację demonstrującą użycie pul wątków.

Rozwiązanie

build.gradle
src
└── main
    └── java
        └── dev
            └── galinski
                └── jedendziennie
                    └── threadpool
                        └── App.java

App.java

package dev.galinski.jedendziennie.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class App {
    public static void main(String[] args) {
        System.out.println("main");
        int poolSize = 2;  // max 2 zadania równocześnie
        System.out.println(String.format("Running in pool of %d threads", 
            poolSize));
        ExecutorService pool = Executors.newFixedThreadPool(poolSize);
        for(int i=0; i < 5; i++) {  // wszystkich zadań jest 5
            pool.execute(new Task(i));
        }
        pool.shutdown();  // koniec przyjmowania nowych zadań
    }   
}


class Task implements Runnable {
    String name;

    Task(Integer id) {
        this.name = "task" + Integer.toString(id);
        System.out.println("Created task " + this.name);
    }

    public void run() {
        int i = 3;
        while (i > 0) {
            try { Thread.sleep(300); }
            catch (InterruptedException e) {}
            
            // przedstaw się
            String threadName = Thread.currentThread().getName();
            String message = String.format("%s %s %d", threadName, name, i);
            System.out.println(message);
            i--;
        }
    }
}

build.gradle

plugins {
    id 'java'
    id 'application'
}

application {
    mainClassName = 'dev.galinski.jedendziennie.threadpool.App'
}

…trzeba przyznać, minimalny build.gradle dla Javy jest prosty.

Uruchamianie

$ gradle run

> Task :run
main
Running in pool of 2 threads
Created task task0
Created task task1
Created task task2
Created task task3
Created task task4
pool-1-thread-1 task0 3
pool-1-thread-2 task1 3
pool-1-thread-1 task0 2
pool-1-thread-2 task1 2
pool-1-thread-1 task0 1
pool-1-thread-2 task1 1
pool-1-thread-1 task2 3
pool-1-thread-2 task3 3
pool-1-thread-1 task2 2
pool-1-thread-2 task3 2
pool-1-thread-1 task2 1
pool-1-thread-2 task3 1
pool-1-thread-1 task4 3
pool-1-thread-1 task4 2
pool-1-thread-1 task4 1

Jak widać, równocześnie wykonują się najwyżej 2 zadania, pozostałe czekają w kolejce.

Więcej o pulach wątków

Pula wątków pozwala zarządzać zasobami przeznaczanymi na równoczesne wykonanie zadań - głównie przez wielokrotne używanie obiektów wątków. Korzystanie z pul wątków z java.util.concurrent odbywa się za pośrednictwem obiektów ExecutorService, tworzonych np. metodami fabrykującymi z klasy Executors:

  • newFixedThreadPool(int n) - pula o stałej liczbie wątków dzielących jedną kolejkę zadań. Wątki, które zginą (np. z powodu wyjątku) są zastępowane przez nowe, które podejmują następne zadania.
  • newCachedThreadPool() - wątki, które utworzy są trzymane w gotowości przez 60 sekund; jeśli w tym czasie pojawi się nowe zadanie, wątek jest używany ponownie, jeśli nie - zostaje usunięty.
  • newSingleThreadExecutor() - jeden wątek, wykonanie sekwencyjne.
  • newScheduledThreadPool(int n), newSingleThreadScheduledExecutor() - pozwalają tworzyć ScheduledExecutorService, czyli egzekutory, które mogą kolejkować zadania opóźnione i cykliczne.
  • newWorkStealingPool(int n) - liczba wątków może zmieniać się dynamicznie, ale nie przekroczy n. To nakładka na ForkJoinPool: każdy procesor ma swoją kolejkę zadań, ale dostęp do niej jest współdzielony. “Jeśli zużyję swoje zadania, a kolega jest zajęty, mogę mu podkraść zadanie”. Ten rodzaj puli nie gwarantuje stabilnej kolejności wykonania zadań.

Jeśli “zwykłe” metody fabrykujące z Executors nie wystarczają), zawsze można użyć bardziej szczegółowego interfejsu ThreadPoolExecutor(). Więcej w dokumentacji Oracle.

Rozmaitości

  • Po chwili programowania w Kotlinie i Scali Java 8 wydaje się archaiczna i koszmarnie rozwlekła. Denerwuje zwłaszcza brak zmiennych automatycznych (na szczęście już od wersji 10 można pisać var msg = "Ala ma kota".
  • Czym różni się interfejs Runnable od Callable?Runnable jest starszy, prostszy i ma mniej funkcjonalności (nie może zwrócić wartości, nie może delegować specjalnych wyjątków).

Źródła