Miért van szüksége ExecutorService-re 1 szálhoz?

Az Executors.newSingleThreadExecutor metódussal létrehozhat egy ExecutorService-t egyetlen szálat tartalmazó készlettel. A medence logikája a következő:

  • A szolgáltatás egyszerre csak egy feladatot hajt végre.
  • Ha N feladatot küldünk be végrehajtásra, akkor az összes N feladatot egymás után hajtja végre az egyetlen szál.
  • Ha a szál megszakad, egy új szál jön létre a fennmaradó feladatok végrehajtásához.

Képzeljünk el egy olyan helyzetet, amikor a programunk a következő funkciókat igényli:

A felhasználói kérelmeket 30 másodpercen belül kell feldolgoznunk, de időegységenként legfeljebb egy kérést.

Feladatosztályt hozunk létre a felhasználói kérések feldolgozásához:


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

Az osztály modellezi a bejövő kérések feldolgozásának viselkedését, és megjeleníti a számát.

Ezután a metódusban létrehozunk egy ExecutorService-t 1 szálhoz, amelyet a bejövő kérések szekvenciális feldolgozására fogunk használni. Mivel a feladat feltételei „30 másodpercen belül” írják elő, 30 másodperces várakozást adunk hozzá, majd erőszakkal leállítjuk az ExecutorService-t .


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

A program elindítása után a konzol üzeneteket jelenít meg a kérés feldolgozásáról:

Feldolgozott kérelem #0 a szálon id=16
Feldolgozott kérelem #1 a szálon id=16
Feldolgozott kérelem #2 a szálon id=16

Feldolgozott kérelem #29 a szálon id=16

A kérések 30 másodperces feldolgozása után az executorService meghívja a shutdownNow() metódust, amely leállítja az aktuális feladatot (a végrehajtás alatt állót), és törli az összes függőben lévő feladatot. Ezt követően a program sikeresen befejeződik.

De nem mindig minden olyan tökéletes, mert könnyen előfordulhat, hogy a programunkban az a helyzet, hogy a medencénk egyetlen szála által felvett feladatok egyike rosszul működik, és akár meg is szakítja a szálunkat. Ezt a helyzetet szimulálhatjuk, hogy kitaláljuk, hogyan működik az executorService egyetlen szálon ebben az esetben.

Ehhez az egyik feladat végrehajtása közben leállítjuk a szálunkat a nem biztonságos és elavult Thread.currentThread().stop() metódussal. Ezt szándékosan tesszük, hogy szimuláljuk azt a helyzetet, amikor az egyik feladat leállítja a szálat.

Megváltoztatjuk a futási metódust a Task osztályban:


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

Megszakítjuk az 5. feladatot.

Nézzük meg, hogyan néz ki a kimenet, ha a szál megszakad az 5. feladat végén:

Feldolgozott kérelem #0 a szálon id=16
Feldolgozott kérelem #1 a szálon id=16
Feldolgozott kérelem #2 a szálon id=16
Feldolgozott kérelem #3 a szálon id=16
Feldolgozott kérelem #4 a szálon id=16
Feldolgozott kérelem #6 a következőn: szál id=17
Feldolgozott kérés #7 a szálon id=17

Feldolgozott kérés #29 szál id=17

Azt látjuk, hogy miután a szál megszakad az 5. feladat végén, a feladatok végrehajtása egy olyan szálban kezdődik, amelynek azonosítója 17, bár korábban a 16-os azonosítójú szálon hajtották végre. És mivel a készletünkben van egy egyetlen szál, ez csak egy dolgot jelenthet: az executorService lecserélte a leállított szálat egy újra, és folytatta a feladatok végrehajtását.

Ezért a newSingleThreadExecutort egyszálú készlettel kell használnunk , ha a feladatokat egymás után és csak egyenként szeretnénk feldolgozni, és a sorból szeretnénk a feladatok feldolgozását folytatni, függetlenül az előző feladat befejezésétől (pl. feladataink közül megöli a fonalat).

ThreadFactory

Amikor a szálak létrehozásáról és újbóli létrehozásáról beszélünk, nem tehetjük meg, hogy megemlítjükThreadFactory.

AThreadFactoryegy olyan objektum, amely igény szerint új szálakat hoz létre.

Létrehozhatjuk saját szállétrehozó gyárunkat, és átadhatunk belőle egy példányt az Executors.newSingleThreadExecutor(ThreadFactory threadFactory) metódusnak.


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Felülírjuk az új szál létrehozásának metódusát, átadva a szál nevét a konstruktornak.

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;
            }
        });
                    
Módosítottuk a létrehozott szál nevét és prioritását.

Tehát azt látjuk, hogy 2 túlterhelt Executors.newSingleThreadExecutor metódusunk van. Az egyik paraméterek nélkül, a másik pedig egy ThreadFactory paraméterrel.

A ThreadFactory segítségével szükség szerint konfigurálhatja a létrehozott szálakat, például prioritások beállításával, szálalosztályok használatával, egy UncaughtExceptionHandler hozzáadásával a szálhoz stb.