Java >> Java tutorial >  >> Tag >> new

Executors newCachedThreadPool() vs newFixedThreadPool()

1. Oversigt

Når det kommer til implementering af trådpool, giver Java-standardbiblioteket masser af muligheder at vælge imellem. De faste og cachelagrede trådpuljer er temmelig allestedsnærværende blandt disse implementeringer.

I denne vejledning skal vi se, hvordan trådpuljer fungerer under hætten, og derefter sammenligne disse implementeringer og deres anvendelsesmuligheder.

2. Cachelagret trådpulje

Lad os tage et kig på, hvordan Java opretter en cachelagret trådpulje, når vi kalder Executors.newCachedThreadPool() :

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

Cachelagrede trådpuljer bruger "synkron overdragelse" til at sætte nye opgaver i kø. Den grundlæggende idé med synkron overdragelse er enkel og alligevel kontraintuitiv:Man kan sætte et emne i kø, hvis og kun hvis en anden tråd tager det emne på samme tid. Med andre ord, den Synchronous Queue  kan ikke varetage nogen som helst opgaver.

Antag, at der kommer en ny opgave ind. Hvis der er en ledig tråd, der venter i køen, så afleverer opgaveproducenten opgaven til den tråd. Ellers, da køen altid er fuld, opretter udføreren en ny tråd til at håndtere denne opgave .

Den cachelagrede pulje starter med nul tråde og kan potentielt vokse til at have heltal.MAX_VALUE  tråde. Praktisk talt er den eneste begrænsning for en cachelagret trådpulje de tilgængelige systemressourcer.

For bedre at administrere systemressourcer vil cachelagrede trådpuljer fjerne tråde, der forbliver inaktive i et minut.

2.1. Use Cases

Den cachelagrede trådpuljekonfiguration cacher trådene (deraf navnet) i et kort stykke tid for at genbruge dem til andre opgaver. Som et resultat fungerer det bedst, når vi har at gøre med et rimeligt antal kortvarige opgaver.

Nøglen her er "rimelig" og "kortvarig". For at præcisere dette punkt, lad os evaluere et scenario, hvor cachelagrede puljer ikke passer godt. Her skal vi indsende en million opgaver, der hver tager 100 mikrosekunder at afslutte:

Callable<String> task = () -> {
    long oneHundredMicroSeconds = 100_000;
    long startedAt = System.nanoTime();
    while (System.nanoTime() - startedAt <= oneHundredMicroSeconds);

    return "Done";
};

var cachedPool = Executors.newCachedThreadPool();
var tasks = IntStream.rangeClosed(1, 1_000_000).mapToObj(i -> task).collect(toList());
var result = cachedPool.invokeAll(tasks);

Dette kommer til at skabe en masse tråde, der oversættes til urimelig hukommelsesbrug, og endnu værre, masser af CPU-kontekstskift. Begge disse uregelmæssigheder ville skade den overordnede ydeevne betydeligt.

Derfor bør vi undgå denne trådpulje, når udførelsestiden er uforudsigelig, f.eks. IO-bundne opgaver.

3. Fixed Thread Pool

Lad os se, hvordan faste trådpuljer fungerer under emhætten:

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

I modsætning til den cachelagrede trådpulje bruger denne en ubegrænset kø med et fast antal aldrig-udløbende tråde. Derfor, i stedet for et stadigt stigende antal tråde, forsøger den faste trådpulje at udføre indgående opgaver med en fast mængde tråde . Når alle tråde er optaget, vil udføreren sætte nye opgaver i kø. På denne måde har vi mere kontrol over vores programs ressourceforbrug.

Som et resultat er faste trådpuljer bedre egnet til opgaver med uforudsigelige udførelsestider.

4. Uheldige ligheder

Indtil videre har vi kun opregnet forskellene mellem cachelagrede og faste trådpuljer.

Bortset fra alle disse forskelle, bruger de begge AbortPolicy som deres mætningspolitik. Derfor forventer vi, at disse eksekutører kaster en undtagelse, når de ikke kan acceptere og endda lægge flere opgaver i kø.

Lad os se, hvad der sker i den virkelige verden.

Cachelagrede trådpuljer vil fortsætte med at skabe flere og flere tråde under ekstreme omstændigheder, så praktisk taget vil de aldrig nå et mætningspunkt . På samme måde vil faste trådpuljer fortsætte med at tilføje flere og flere opgaver i deres kø. Derfor vil de faste puljer heller aldrig nå et mætningspunkt .

Da begge puljer ikke vil være mættede, når belastningen er usædvanlig høj, vil de forbruge meget hukommelse til at oprette tråde eller stille opgaver i kø. Cachelagrede trådpuljer tilføjer fornærmelse til skaden og vil også medføre en masse processorkontekstskift.

Uanset hvad, for at have mere kontrol over ressourceforbruget, anbefales det stærkt at oprette en tilpasset  ThreadPoolExecutor :

var boundedQueue = new ArrayBlockingQueue<Runnable>(1000);
new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy());

Her kan vores trådpulje have op til 20 tråde og kan kun stille op til 1000 opgaver i kø. Også, når den ikke kan acceptere mere belastning, vil den simpelthen give en undtagelse.

5. Konklusion

I dette selvstudie fik vi et kig ind i JDK-kildekoden for at se, hvor forskellige Udførere  arbejde under hætten. Derefter sammenlignede vi de faste og cachelagrede trådpuljer og deres use-cases.

Til sidst forsøgte vi at adressere disse poolers ude af kontrol med ressourceforbruget med tilpassede trådpuljer.


Java tag