Tại sao bạn có thể cần ExecutorService cho 1 luồng?

Bạn có thể sử dụng phương thức Executor.newSingleThreadExecutor để tạo một ExecutorService với một nhóm bao gồm một luồng đơn. Logic của pool như sau:

  • Dịch vụ chỉ thực hiện một nhiệm vụ tại một thời điểm.
  • Nếu chúng tôi gửi N tác vụ để thực thi, tất cả N tác vụ sẽ được thực hiện lần lượt bởi một luồng.
  • Nếu luồng bị gián đoạn, một luồng mới sẽ được tạo để thực hiện mọi tác vụ còn lại.

Hãy tưởng tượng một tình huống mà chương trình của chúng ta yêu cầu chức năng sau:

Chúng tôi cần xử lý các yêu cầu của người dùng trong vòng 30 giây, nhưng không quá một yêu cầu trên mỗi đơn vị thời gian.

Chúng tôi tạo một lớp tác vụ để xử lý yêu cầu của người dùng:


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

Lớp mô hình hóa hành vi xử lý một yêu cầu đến và hiển thị số của nó.

Tiếp theo, trong phương thức chính , chúng tôi tạo ExecutorService cho 1 luồng, chúng tôi sẽ sử dụng luồng này để xử lý tuần tự các yêu cầu đến. Vì các điều kiện tác vụ quy định "trong vòng 30 giây", chúng tôi thêm thời gian chờ 30 giây rồi buộc dừng 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();
}
    

Sau khi bắt đầu chương trình, bảng điều khiển hiển thị các thông báo về xử lý yêu cầu:

Yêu cầu đã xử lý #0 trên chuỗi id=16
Yêu cầu đã xử lý #1 trên chuỗi id=16
Yêu cầu đã xử lý #2 trên chuỗi id=16

Yêu cầu đã xử lý #29 trên chuỗi id=16

Sau khi xử lý các yêu cầu trong 30 giây, executorService gọi phương thức shutdownNow() , phương thức này sẽ dừng tác vụ hiện tại (tác vụ đang được thực thi) và hủy tất cả các tác vụ đang chờ xử lý. Sau đó, chương trình kết thúc thành công.

Nhưng mọi thứ không phải lúc nào cũng hoàn hảo như vậy, bởi vì chương trình của chúng ta có thể dễ dàng xảy ra tình huống trong đó một trong các tác vụ được chọn bởi chuỗi duy nhất của nhóm của chúng ta hoạt động không chính xác và thậm chí chấm dứt chuỗi của chúng ta. Chúng ta có thể mô phỏng tình huống này để tìm ra cách executorService hoạt động với một luồng duy nhất trong trường hợp này.

Để thực hiện việc này, trong khi một trong các tác vụ đang được thực thi, chúng ta kết thúc chuỗi của mình bằng phương thức Thread.currentThread().stop() không an toàn và lỗi thời . Chúng tôi đang cố tình làm điều này để mô phỏng tình huống trong đó một trong các tác vụ kết thúc chuỗi.

Chúng ta sẽ thay đổi phương thức run trong lớp Task :


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

Chúng tôi sẽ làm gián đoạn nhiệm vụ số 5.

Hãy xem đầu ra trông như thế nào với luồng bị gián đoạn ở cuối nhiệm vụ #5:

Yêu cầu đã xử lý #0 trên chuỗi id=16
Yêu cầu đã xử lý #1 trên chuỗi id=16
Yêu cầu đã xử lý #2 trên chuỗi id=16
Yêu cầu đã xử lý #3 trên chuỗi id=16 Yêu cầu
đã xử lý #4 trên chuỗi id=16
Yêu cầu đã xử lý #6 trên chuỗi thread id=17
Đã xử lý yêu cầu #7 trên luồng id=17

Đã xử lý yêu cầu #29 trên luồng id=17

Chúng tôi thấy rằng sau khi luồng bị gián đoạn ở cuối tác vụ 5, các tác vụ bắt đầu được thực thi trong một luồng có mã định danh là 17, mặc dù trước đó chúng đã được thực thi trên luồng có mã định danh là 16. Và bởi vì nhóm của chúng tôi có một một luồng duy nhất, điều này chỉ có nghĩa là một điều: executorService đã thay thế luồng đã dừng bằng một luồng mới và tiếp tục thực hiện các tác vụ.

Vì vậy, chúng ta nên sử dụng newSingleThreadExecutor với nhóm một luồng khi chúng ta muốn xử lý các tác vụ theo tuần tự và chỉ một tác vụ tại một thời điểm và chúng ta muốn tiếp tục xử lý các tác vụ từ hàng đợi bất kể việc hoàn thành tác vụ trước đó (ví dụ: trường hợp một trong số các nhiệm vụ của chúng tôi sẽ giết chết chuỗi).

chủ đềFactory

Nói đến tạo và tái tạo thread, chúng ta không thể không nhắc đếnchủ đềFactory.

MỘTchủ đềFactorylà một đối tượng tạo chủ đề mới theo yêu cầu.

Chúng ta có thể tạo nhà máy tạo luồng của riêng mình và chuyển một phiên bản của nó tới phương thức Executors.newSingleThreadExecutor(ThreadFactory threadFactory) .


ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "MyThread");
            }
        });
                    
Chúng tôi ghi đè phương thức tạo luồng mới, chuyển tên luồng cho hàm tạo.

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;
            }
        });
                    
Chúng tôi đã thay đổi tên và mức độ ưu tiên của chuỗi đã tạo.

Vì vậy, chúng tôi thấy rằng chúng tôi có 2 phương thức Executor.newSingleThreadExecutor quá tải . Một cái không có tham số và cái thứ hai có tham số ThreadFactory .

Sử dụng ThreadFactory , bạn có thể định cấu hình các luồng đã tạo khi cần, ví dụ: bằng cách đặt mức độ ưu tiên, sử dụng các lớp con của luồng, thêm một UncaughtExceptionHandler vào luồng, v.v.