Tại sao bạn cần giao diện Executor?

Trước Java 5, bạn phải viết tất cả quản lý chuỗi mã của riêng mình trong ứng dụng của mình. Ngoài ra, việc tạo ra mộtchủ đề mớiđối tượng là một hoạt động sử dụng nhiều tài nguyên và sẽ không hợp lý khi tạo một luồng mới cho mọi tác vụ nhẹ. Và bởi vì vấn đề này hoàn toàn quen thuộc với mọi nhà phát triển ứng dụng đa luồng, nên họ đã quyết định đưa chức năng này vào Java dưới dạng khung Executor .

Ý tưởng lớn là gì? Thật đơn giản: thay vì tạo một luồng mới cho mỗi tác vụ mới, các luồng được giữ trong một loại "bộ lưu trữ" và khi một tác vụ mới đến, chúng tôi sẽ truy xuất một luồng hiện có thay vì tạo một luồng mới.

Các giao diện chính của khung này là Executor , ExecutorServiceScheduledExecutorService , mỗi giao diện mở rộng chức năng của giao diện trước đó.

Giao diện Executor là giao diện cơ sở. Nó khai báo một phương thức thực thi void (lệnh Runnable) duy nhất được triển khai bởi một đối tượng Runnable .

Giao diện ExecutorService thú vị hơn. Nó có các phương thức để quản lý việc hoàn thành công việc, cũng như các phương thức trả về một số loại kết quả. Chúng ta hãy xem xét kỹ hơn các phương pháp của nó:

Phương pháp Sự miêu tả
vô hiệu tắt máy (); Gọi phương thức này sẽ dừng ExecutorService . Tất cả các nhiệm vụ đã được gửi để xử lý sẽ được hoàn thành, nhưng các nhiệm vụ mới sẽ không được chấp nhận.
Danh sách<Runnable> shutdownNow();

Gọi phương thức này sẽ dừng ExecutorService . Thread.interrupt sẽ được gọi cho tất cả các tác vụ đã được gửi để xử lý. Phương thức này trả về một danh sách các tác vụ được xếp hàng đợi.

Phương thức không đợi hoàn thành tất cả các tác vụ đang "đang tiến hành" tại thời điểm phương thức được gọi.

Cảnh báo: Gọi phương thức này có thể làm rò rỉ tài nguyên.

boolean isShutdown(); Kiểm tra xem ExecutorService có bị dừng hay không.
boolean isTerminated(); Trả về true nếu tất cả các tác vụ đã được hoàn thành sau khi tắt ExecutorService . Cho đến khi shutdown() hoặc shutdownNow() được gọi, nó sẽ luôn trả về false .
boolean awaitTermination(thời gian chờ dài, đơn vị TimeUnit) ném ngoại lệ gián đoạn;

Sau khi phương thức shutdown() được gọi, phương thức này chặn chuỗi mà nó được gọi, cho đến khi một trong các điều kiện sau là đúng:

  • tất cả các nhiệm vụ theo lịch trình đã hoàn thành;
  • thời gian chờ được truyền cho phương thức đã hết;
  • chủ đề hiện tại bị gián đoạn.

Trả về true nếu tất cả các tác vụ đã hoàn thành và false nếu hết thời gian chờ trước khi kết thúc.

<T> Future<T> submit(Callable<T> task);

Thêm một tác vụ Có thể gọi được vào ExecutorService và trả về một đối tượng triển khai giao diện Tương lai .

<T> là loại kết quả của nhiệm vụ được thông qua.

<T> Future<T> submit(Tác vụ có thể chạy được, kết quả T);

Thêm một tác vụ Runnable vào ExecutorService và trả về một đối tượng triển khai giao diện Tương lai .

Tham số kết quả T là những gì được trả về khi gọi phương thức get() trên kết quảĐối tượng tương lai.

Future<?> submit(Runnable task);

Thêm một tác vụ Runnable vào ExecutorService và trả về một đối tượng triển khai giao diện Tương lai .

Nếu chúng ta gọi phương thức get() trên đối tượng Future kết quả , thì chúng ta sẽ nhận được null.

Danh sách <T> <Tương lai <T>> invokeAll (Bộ sưu tập <? mở rộng Nhiệm vụ có thể gọi <T>>) ném InterruptedException;

Chuyển danh sách các tác vụ Có thể gọi tới ExecutorService . Trả về một danh sách Tương lai mà từ đó chúng ta có thể nhận được kết quả của công việc. Danh sách này được trả về khi tất cả các nhiệm vụ đã gửi được hoàn thành.

Nếu bộ sưu tập nhiệm vụ được sửa đổi trong khi phương pháp đang chạy, kết quả của phương pháp này là không xác định.

<T> Danh sách <Tương lai<T>> invokeAll(Bộ sưu tập <? mở rộng các nhiệm vụ Có thể gọi<T>>, thời gian chờ dài, đơn vị TimeUnit) ném InterruptedException;

Chuyển danh sách các tác vụ Có thể gọi tới ExecutorService . Trả về một danh sách Tương lai mà từ đó chúng ta có thể nhận được kết quả của công việc. Danh sách này được trả về khi tất cả các tác vụ đã truyền được hoàn thành hoặc sau khi hết thời gian chờ được truyền cho phương thức, tùy theo điều kiện nào đến trước.

Nếu hết thời gian chờ, các tác vụ chưa hoàn thành sẽ bị hủy.

Lưu ý: Có thể một tác vụ bị hủy sẽ không ngừng chạy (chúng ta sẽ thấy tác dụng phụ này trong ví dụ).

Nếu bộ sưu tập nhiệm vụ được sửa đổi trong khi phương pháp đang chạy, kết quả của phương pháp này là không xác định.

<T> T InvokeAny(Collection<? extends Callable<T>> task) ném InterruptedException, ExecutException;

Chuyển danh sách các tác vụ Có thể gọi tới ExecutorService . Trả về kết quả của một trong các tác vụ (nếu có) đã hoàn thành mà không đưa ra ngoại lệ (nếu có).

Nếu bộ sưu tập nhiệm vụ được sửa đổi trong khi phương pháp đang chạy, kết quả của phương pháp này là không xác định.

<T> T InvokeAny(Bộ sưu tập<? mở rộng các tác vụ có thể gọi<T>>, thời gian chờ dài, đơn vị TimeUnit) ném InterruptedException, ExecutException, TimeoutException;

Chuyển danh sách các tác vụ Có thể gọi tới ExecutorService . Trả về kết quả của một trong các tác vụ (nếu có) đã hoàn thành mà không đưa ra ngoại lệ trước khi hết thời gian chờ được truyền cho phương thức.

Nếu bộ sưu tập nhiệm vụ được sửa đổi trong khi phương pháp đang chạy, kết quả của phương pháp này là không xác định.

Hãy xem một ví dụ nhỏ về cách làm việc với ExecutorService .


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Đầu ra:

done
done
Tương lai nhận được sleep 1
: giấc ngủ bị gián đoạn
giấc ngủ 1: giấc ngủ bị gián đoạn
done true true


Mỗi nhiệm vụ chạy trong 5 giây. Chúng tôi đã tạo một nhóm cho hai luồng, vì vậy hai dòng đầu ra đầu tiên có ý nghĩa hoàn hảo.

Sáu giây sau khi chương trình bắt đầu, phương thức invokeAll hết thời gian chờ và kết quả được trả về dưới dạng danh sách Futures . Điều này có thể được nhìn thấy từ chuỗi đầu ra Hợp đồng tương lai nhận được .

Sau khi hoàn thành hai nhiệm vụ đầu tiên, hai nhiệm vụ khác sẽ bắt đầu. Nhưng vì thời gian chờ được đặt trong phương thức invokeAll đã hết, hai tác vụ này không có thời gian để hoàn thành. Họ nhận được lệnh "hủy" . Đó là lý do tại sao đầu ra có hai dòng với trạng thái ngủ 1: giấc ngủ bị gián đoạn .

Và sau đó bạn có thể thấy thêm hai dòng với done . Đây là tác dụng phụ mà tôi đã đề cập khi mô tả phương thức invokeAll .

Nhiệm vụ thứ năm và nhiệm vụ cuối cùng thậm chí không bao giờ được bắt đầu, vì vậy chúng tôi không thấy bất kỳ điều gì về nó trong đầu ra.

Hai dòng cuối cùng là kết quả của việc gọi các phương thức isShutdownisTerminated .

Cũng rất thú vị khi chạy ví dụ này trong chế độ gỡ lỗi và xem trạng thái tác vụ sau khi hết thời gian chờ (đặt điểm ngắt trên dòng với executorService.shutdown(); ):

Chúng tôi thấy rằng hai nhiệm vụ Đã hoàn thành bình thường và ba nhiệm vụ đã bị "Hủy" .

Theo lịch trìnhExecutorDịch vụ

Để kết thúc phần thảo luận của chúng ta về các bộ thực thi, chúng ta hãy xem ScheduledExecutorService .

Nó có 4 phương pháp:

Phương pháp Sự miêu tả
public ScheduledFuture<?> schedule(Lệnh có thể chạy, độ trễ dài, đơn vị TimeUnit); Lên lịch cho tác vụ Runnable đã qua để chạy một lần sau độ trễ được chỉ định làm đối số.
public <V> ScheduledFuture<V> schedule(Callable<V> có thể gọi được, độ trễ dài, đơn vị TimeUnit); Lên lịch cho tác vụ Có thể gọi được đã thông qua để chạy một lần sau độ trễ được chỉ định làm đối số.
public ScheduledFuture<?> scheduleAtFixedRate(Lệnh có thể chạy, initDelay dài, khoảng thời gian dài, đơn vị TimeUnit); Lên lịch thực hiện định kỳ tác vụ đã thông qua, tác vụ này sẽ được thực hiện lần đầu tiên sau initDelay và mỗi lần chạy tiếp theo sẽ bắt đầu sau khoảng thời gian đó .
public ScheduledFuture<?> scheduleWithFixedDelay(Lệnh có thể chạy được, long initialDelay, long delay, đơn vị TimeUnit); Lên lịch thực hiện định kỳ tác vụ đã thông qua, tác vụ này sẽ được thực hiện lần đầu tiên sau initDelay và mỗi lần chạy tiếp theo sẽ bắt đầu sau độ trễ (khoảng thời gian giữa khi hoàn thành lần chạy trước và bắt đầu lần chạy hiện tại).