Waarom zou u een ExecutorService nodig hebben voor 1 thread?

U kunt de methode Executors.newSingleThreadExecutor gebruiken om een ​​ExecutorService te maken met een pool die één thread bevat. De logica van het zwembad is als volgt:

  • De service voert slechts één taak tegelijk uit.
  • Als we N taken indienen voor uitvoering, worden alle N taken één voor één uitgevoerd door de enkele thread.
  • Als de thread wordt onderbroken, wordt er een nieuwe thread gemaakt om eventuele resterende taken uit te voeren.

Laten we ons een situatie voorstellen waarin ons programma de volgende functionaliteit vereist:

We moeten gebruikersverzoeken binnen 30 seconden verwerken, maar niet meer dan één verzoek per tijdseenheid.

We maken een taakklasse voor het verwerken van een gebruikersverzoek:


class Task implements Runnable {
   private final int taskNumber;

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

   @Override
   public void run() {
       try {
           Thread.sleep(1000);
       } catch (InterruptedException ignored) {
       }
       System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
   }
}
    

De klasse modelleert het gedrag van het verwerken van een inkomend verzoek en geeft het nummer weer.

Vervolgens maken we in de hoofdmethode een ExecutorService voor 1 thread, die we zullen gebruiken om inkomende verzoeken opeenvolgend te verwerken. Aangezien de taakvoorwaarden "binnen 30 seconden" bepalen, voegen we een wachttijd van 30 seconden toe en stoppen we vervolgens met geweld de ExecutorService .


public static void main(String[] args) throws InterruptedException {
   ExecutorService executorService = Executors.newSingleThreadExecutor();

   for (int i = 0; i < 1_000; i++) {
       executorService.execute(new Task(i));
   }
   executorService.awaitTermination(30, TimeUnit.SECONDS);
   executorService.shutdownNow();
}
    

Nadat het programma is gestart, geeft de console berichten weer over de verwerking van verzoeken:

Verwerkt verzoek #0 op thread id=16
Verwerkt verzoek #1 op thread id=16
Verwerkt verzoek #2 op thread id=16

Verwerkt verzoek #29 op thread id=16

Nadat verzoeken gedurende 30 seconden zijn verwerkt, roept executorService de methode shutdownNow() aan , die de huidige taak (de taak die wordt uitgevoerd) stopt en alle lopende taken annuleert. Daarna wordt het programma succesvol afgesloten.

Maar alles is niet altijd zo perfect, omdat ons programma gemakkelijk een situatie kan hebben waarin een van de taken die door de enige thread van onze pool worden opgepikt, niet goed werkt en zelfs onze thread beëindigt. We kunnen deze situatie simuleren om erachter te komen hoe executorService in dit geval werkt met een enkele thread.

Om dit te doen, terwijl een van de taken wordt uitgevoerd, beëindigen we onze thread met behulp van de onveilige en verouderde Thread.currentThread().stop() methode. We doen dit met opzet om de situatie te simuleren waarin een van de taken de thread beëindigt.

We zullen de run- methode in de Task- klasse wijzigen:


@Override
public void run() {
   try {
       Thread.sleep(1000);
   } catch (InterruptedException ignored) {
   }

   if (taskNumber == 5) {
       Thread.currentThread().stop();
   }

   System.out.printf("Processed request #%d on thread id=%d\\n", taskNumber, Thread.currentThread().getId());
}
    

We onderbreken taak #5.

Laten we eens kijken hoe de uitvoer eruitziet met de thread onderbroken aan het einde van taak #5:

Verwerkt verzoek #0 op thread id=16
Verwerkt verzoek #1 op thread id=16
Verwerkt verzoek #2 op thread id=16
Verwerkt verzoek #3 op thread id=16
Verwerkt verzoek #4 op thread id=16
Verwerkt verzoek #6 op thread id=17
Verwerkt verzoek #7 op thread id=17

Verwerkt verzoek #29 op thread id=17

We zien dat nadat de thread is onderbroken aan het einde van taak 5, de taken worden uitgevoerd in een thread waarvan de identifier 17 is, hoewel ze eerder waren uitgevoerd op de thread met de identifier 16. En omdat onze pool een enkele thread kan dit maar één ding betekenen: executorService heeft de gestopte thread vervangen door een nieuwe en is doorgegaan met het uitvoeren van de taken.

We zouden dus newSingleThreadExecutor moeten gebruiken met een single-thread pool als we taken opeenvolgend en slechts één voor één willen verwerken, en we willen doorgaan met het verwerken van taken uit de wachtrij ongeacht de voltooiing van de vorige taak (bijvoorbeeld het geval dat een van onze taken doodt de rode draad).

Draadfabriek

Als we het hebben over het maken en opnieuw maken van threads, kunnen we het niet laten om te vermeldenDraadfabriek.

ADraadfabriekis een object dat op verzoek nieuwe threads maakt.

We kunnen onze eigen fabriek voor het maken van threads maken en er een exemplaar van doorgeven aan de methode Executors.newSingleThreadExecutor (ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
We overschrijven de methode voor het maken van een nieuwe thread door een threadnaam door te geven aan de constructor.

ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "MyThread");
                thread.setPriority(Thread.MAX_PRIORITY);
                return thread;
            }
        });
                    
We hebben de naam en prioriteit van de aangemaakte thread gewijzigd.

We zien dus dat we 2 overbelaste Executors.newSingleThreadExecutor -methoden hebben. Een zonder parameters en een tweede met een ThreadFactory- parameter.

Met behulp van een ThreadFactory kunt u de gemaakte threads naar behoefte configureren, bijvoorbeeld door prioriteiten in te stellen, thread-subklassen te gebruiken, een UncaughtExceptionHandler aan de thread toe te voegen, enzovoort.