Mengapa Anda membutuhkan ExecutorService untuk 1 utas?

Anda dapat menggunakan metode Executors.newSingleThreadExecutor untuk membuat ExecutorService dengan kumpulan yang mencakup satu utas. Logika kolam adalah sebagai berikut:

  • Layanan hanya mengeksekusi satu tugas pada satu waktu.
  • Jika kami mengirimkan N tugas untuk dieksekusi, semua tugas N akan dieksekusi satu demi satu oleh utas tunggal.
  • Jika utas terputus, utas baru akan dibuat untuk menjalankan tugas yang tersisa.

Bayangkan sebuah situasi di mana program kita membutuhkan fungsionalitas berikut:

Kami perlu memproses permintaan pengguna dalam 30 detik, tetapi tidak lebih dari satu permintaan per unit waktu.

Kami membuat kelas tugas untuk memproses permintaan pengguna:


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

Kelas memodelkan perilaku memproses permintaan masuk dan menampilkan nomornya.

Selanjutnya, dalam metode utama , kami membuat ExecutorService untuk 1 utas, yang akan kami gunakan untuk memproses permintaan masuk secara berurutan. Karena kondisi tugas menetapkan "dalam 30 detik", kami menambahkan menunggu 30 detik dan kemudian menghentikan ExecutorService secara paksa .


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

Setelah memulai program, konsol menampilkan pesan tentang pemrosesan permintaan:

Permintaan diproses #0 di thread id=16
Permintaan diproses #1 di thread id=16
Permintaan diproses #2 di thread id=16

Permintaan diproses #29 di thread id=16

Setelah memproses permintaan selama 30 detik, executorService memanggil metode shutdownNow() , yang menghentikan tugas saat ini (yang sedang dieksekusi) dan membatalkan semua tugas yang tertunda. Setelah itu, program berakhir dengan sukses.

Tetapi semuanya tidak selalu begitu sempurna, karena program kami dapat dengan mudah mengalami situasi di mana salah satu tugas yang diambil oleh satu-satunya utas kumpulan kami tidak berfungsi dengan benar dan bahkan menghentikan utas kami. Kami dapat mensimulasikan situasi ini untuk mengetahui bagaimana executorService bekerja dengan satu utas dalam kasus ini.

Untuk melakukan ini, saat salah satu tugas sedang dieksekusi, kami mengakhiri utas kami menggunakan metode Thread.currentThread().stop() yang tidak aman dan usang . Kami melakukan ini dengan sengaja untuk mensimulasikan situasi di mana salah satu tugas mengakhiri utas.

Kami akan mengubah metode jalankan di kelas Tugas :


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

Kami akan menyela tugas #5.

Mari kita lihat seperti apa hasilnya dengan utas yang terputus di akhir tugas #5:

Permintaan yang diproses #0 pada id utas=16
Permintaan yang diproses #1 pada id utas=16
Permintaan yang diproses #2 pada id utas=16
Permintaan yang diproses #3 pada id utas=16 Permintaan
yang diproses #4 pada id utas=16 Permintaan
yang diproses #6 pada thread id=17
Permintaan diproses #7 di thread id=17

Permintaan diproses #29 di thread id=17

Kami melihat bahwa setelah utas terputus di akhir tugas 5, tugas mulai dieksekusi di utas yang pengenalnya 17, meskipun sebelumnya telah dieksekusi di utas dengan pengenal 16. Dan karena kumpulan kami memiliki utas tunggal, ini hanya dapat berarti satu hal: executorService mengganti utas yang dihentikan dengan yang baru dan terus menjalankan tugas.

Jadi, kita harus menggunakan newSingleThreadExecutor dengan kumpulan utas tunggal saat kita ingin memproses tugas secara berurutan dan hanya satu per satu, dan kita ingin melanjutkan pemrosesan tugas dari antrian terlepas dari selesainya tugas sebelumnya (misalnya kasus di mana satu tugas kami membunuh utas).

ThreadFactory

Saat berbicara tentang membuat dan membuat ulang utas, kami tidak bisa tidak menyebutkannyaThreadFactory.

AThreadFactoryadalah objek yang membuat utas baru sesuai permintaan.

Kita dapat membuat pabrik pembuatan utas kita sendiri dan meneruskan turunannya ke metode Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Kami mengganti metode untuk membuat utas baru, meneruskan nama utas ke konstruktor.

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;
            }
        });
                    
Kami mengubah nama dan prioritas utas yang dibuat.

Jadi kita melihat bahwa kita memiliki 2 metode Executors.newSingleThreadExecutor yang kelebihan beban . Satu tanpa parameter, dan satu lagi dengan parameter ThreadFactory .

Menggunakan ThreadFactory , Anda dapat mengonfigurasi utas yang dibuat sesuai kebutuhan, misalnya, dengan menyetel prioritas, menggunakan subkelas utas, menambahkan UncaughtExceptionHandler ke utas, dan seterusnya.