Phương thức newFixedThreadPool của lớp Executors tạo một executorService với số luồng cố định. Không giống như phương thức newSingleThreadExecutor , chúng tôi chỉ định số lượng luồng chúng tôi muốn trong nhóm. Dưới mui xe, mã sau đây được gọi là:

new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

Các tham số corePoolSize (số lượng luồng sẽ sẵn sàng (bắt đầu) khi dịch vụ thực thi bắt đầu) và maxPoolSize (số lượng luồng tối đa mà dịch vụ thực thi có thể tạo) nhận cùng một giá trị — số lượng luồng được chuyển đến newFixedThreadPool(nThreads ) . Và chúng ta có thể vượt qua việc triển khai ThreadFactory của riêng mình theo cách chính xác như vậy.

Chà, hãy xem tại sao chúng ta cần một ExecutorService như vậy .

Đây là logic của ExecutorService với số (n) luồng cố định:

  • Tối đa n luồng sẽ hoạt động để xử lý các tác vụ.
  • Nếu có hơn n nhiệm vụ được gửi, chúng sẽ được giữ trong hàng đợi cho đến khi các luồng trở nên rảnh rỗi.
  • Nếu một trong các luồng bị lỗi và kết thúc, một luồng mới sẽ được tạo để thay thế.
  • Bất kỳ chuỗi nào trong nhóm đều hoạt động cho đến khi nhóm ngừng hoạt động.

Ví dụ, hãy tưởng tượng bạn đang đợi để qua kiểm tra an ninh tại sân bay. Mọi người đứng thành một hàng cho đến ngay trước khi kiểm tra an ninh, hành khách được phân phối giữa tất cả các trạm kiểm soát đang hoạt động. Nếu có sự chậm trễ tại một trong các điểm kiểm tra, hàng đợi sẽ chỉ được xử lý bởi điểm kiểm tra thứ hai cho đến khi điểm kiểm tra đầu tiên trống. Và nếu một trạm kiểm soát đóng cửa hoàn toàn, thì trạm kiểm soát khác sẽ được mở để thay thế và hành khách sẽ tiếp tục được xử lý qua hai trạm kiểm soát.

Chúng tôi sẽ lưu ý ngay rằng ngay cả khi các điều kiện là lý tưởng — n luồng đã hứa hoạt động ổn định và các luồng kết thúc bằng lỗi luôn được thay thế (điều mà tài nguyên hạn chế không thể đạt được trong một sân bay thực) — hệ thống vẫn có một số các tính năng khó chịu, bởi vì trong mọi trường hợp sẽ không có nhiều luồng hơn, ngay cả khi hàng đợi phát triển nhanh hơn các luồng có thể xử lý tác vụ.

Tôi khuyên bạn nên tìm hiểu thực tế về cách ExecutorService hoạt động với một số luồng cố định. Hãy tạo một lớp triển khai Runnable . Các đối tượng của lớp này đại diện cho các nhiệm vụ của chúng ta đối với ExecutorService .

public class Task implements Runnable {
    int taskNumber;

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

    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}

Trong phương thức run() , chúng ta chặn luồng trong 2 giây, mô phỏng một số khối lượng công việc, sau đó hiển thị số của tác vụ hiện tại và tên của luồng đang thực thi tác vụ.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }

        executorService.shutdown();

Để bắt đầu, trong phương thức chính , chúng tôi tạo một ExecutorService và gửi 30 tác vụ để thực thi.

Yêu cầu người dùng đã xử lý #1 trên luồng pool-1-thread-2
Yêu cầu người dùng đã xử lý #0 trên luồng pool-1-thread-1 Yêu
cầu người dùng đã xử lý #2 trên luồng pool-1-thread-3 Yêu cầu
người dùng đã xử lý #5 trên pool- Chuỗi 1-thread-3
Yêu cầu người dùng đã xử lý #3 trên chuỗi pool-1-thread-2 Yêu
cầu người dùng đã xử lý #4 trên chuỗi pool-1-thread-1
Yêu cầu người dùng đã xử lý #8 trên chuỗi pool-1-thread-1
Người dùng đã xử lý yêu cầu #6 trên luồng pool-1-thread-3
Yêu cầu người dùng đã xử lý #7 trên luồng pool-1-thread-2
Yêu cầu người dùng đã xử lý #10 trên luồng pool-1-thread-3
Yêu cầu người dùng đã xử lý #9 trên pool-1- thread-1 thread
Đã xử lý yêu cầu người dùng #11 trên pool-1-thread-2 thread
Yêu cầu người dùng đã xử lý #12 trên pool-1-thread-3 thread
Yêu cầu người dùng đã xử lý #14 trên luồng pool-1-thread-2
Yêu cầu người dùng đã xử lý #13 trên luồng pool-1-thread-1
Yêu cầu người dùng đã xử lý #15 trên luồng pool-1-thread-3 Yêu cầu
người dùng đã xử lý #16 trên pool- Chuỗi 1-thread-2
Yêu cầu người dùng đã xử lý #17 trên chuỗi pool-1-thread-1
Yêu cầu người dùng đã xử lý #18 trên chuỗi pool-1-thread-3 Yêu
cầu người dùng đã xử lý #19 trên chuỗi pool-1-thread-2 Người
dùng đã xử lý yêu cầu #20 trên luồng pool-1-thread-1
Yêu cầu người dùng đã xử lý #21 trên luồng pool-1-thread-3
Yêu cầu người dùng đã xử lý #22 trên luồng pool-1-thread-2 Yêu
cầu người dùng đã xử lý #23 trên pool-1- thread-1 thread
Đã xử lý yêu cầu người dùng #25 trên pool-1-thread-2 thread
Yêu cầu người dùng đã xử lý #24 trên pool-1-thread-3 thread
Yêu cầu người dùng đã xử lý #26 trên luồng pool-1-thread-1
Yêu cầu người dùng đã xử lý #27 trên luồng pool-1-thread-2
Yêu cầu người dùng đã xử lý #28 trên luồng pool-1-thread-3 Yêu cầu
người dùng đã xử lý #29 trên pool- 1 chủ đề-1 chủ đề

Đầu ra của bàn điều khiển cho chúng ta thấy cách các tác vụ được thực thi trên các luồng khác nhau sau khi chúng được giải phóng bởi tác vụ trước đó.

Bây giờ, chúng tôi sẽ tăng số lượng tác vụ lên 100 và sau khi gửi 100 tác vụ, chúng tôi sẽ gọi phương thức awaitTermination(11, SECONDS) . Chúng tôi chuyển một số và đơn vị thời gian làm đối số. Phương pháp này sẽ chặn luồng chính trong 11 giây. Sau đó, chúng tôi sẽ gọi shutdownNow() để buộc ExecutorService tắt mà không đợi tất cả các tác vụ hoàn thành.

ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }

        executorService.awaitTermination(11, SECONDS);

        executorService.shutdownNow();
        System.out.println(executorService);

Cuối cùng, chúng tôi sẽ hiển thị thông tin về trạng thái của executorService .

Đây là đầu ra giao diện điều khiển mà chúng tôi nhận được:

Yêu cầu người dùng đã xử lý #0 trên luồng pool-1-thread-1
Yêu cầu người dùng đã xử lý #2 trên luồng pool-1-thread-3 Yêu
cầu người dùng đã xử lý #1 trên luồng pool-1-thread-2 Yêu cầu
người dùng đã xử lý #4 trên pool- Chuỗi 1-thread-3
Yêu cầu người dùng đã xử lý #5 trên chuỗi pool-1-thread-2 Yêu
cầu người dùng đã xử lý #3 trên chuỗi pool-1-thread-1
Yêu cầu người dùng đã xử lý #6 trên chuỗi pool-1-thread-3 Người
dùng đã xử lý yêu cầu số 7 trên luồng pool-1-thread-2
Yêu cầu người dùng đã xử lý số 8 trên luồng pool-1-thread-1
Yêu cầu người dùng đã xử lý số 9 trên luồng pool-1-thread-3
Yêu cầu người dùng đã xử lý số 11 trên pool-1- thread-1 thread
Đã xử lý yêu cầu người dùng #10 trên pool-1-thread-2 thread
Yêu cầu người dùng đã xử lý #13 trên pool-1-thread-1 thread
Yêu cầu người dùng đã xử lý #14 trên chuỗi pool-1-thread-2
Yêu cầu người dùng đã xử lý #12 trên chuỗi pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Đang tắt, kích thước nhóm = 3, chuỗi hoạt động = 3 , các tác vụ đã xếp hàng = 0, các tác vụ đã hoàn thành = 15]
Yêu cầu người dùng đã xử lý số 17 trên luồng nhóm-1-luồng-3
Yêu cầu người dùng đã xử lý số 15 trên luồng nhóm-1-luồng-1
Yêu cầu người dùng đã xử lý số 16 trên luồng-nhóm-1 -2 chủ đề

Tiếp theo là 3 InterruptedExceptions , được đưa ra bởi các phương thức ngủ từ 3 tác vụ đang hoạt động.

Ta có thể thấy khi chương trình kết thúc, 15 task đã được thực hiện nhưng pool vẫn còn 3 thread đang hoạt động chưa thực hiện xong task của chúng. Phương thức ngắt() được gọi trên ba luồng này, có nghĩa là tác vụ sẽ hoàn thành, nhưng trong trường hợp của chúng ta, phương thức ngủ sẽ đưa ra một Ngoại lệ gián đoạn . Chúng ta cũng thấy rằng sau khi phương thức shutdownNow() được gọi, hàng đợi tác vụ sẽ bị xóa.

Vì vậy, khi sử dụng ExecutorService với số lượng luồng cố định trong nhóm, hãy nhớ cách thức hoạt động của nó. Loại này phù hợp với các tác vụ có tải không đổi đã biết.

Đây là một câu hỏi thú vị khác: nếu bạn cần sử dụng một bộ thực thi cho một luồng, bạn nên gọi phương thức nào? newSingleThreadExecutor() hay newFixedThreadPool(1) ?

Cả hai người thi hành sẽ có hành vi tương đương. Sự khác biệt duy nhất là phương thức newSingleThreadExecutor() sẽ trả về một bộ thực thi mà sau này không thể cấu hình lại để sử dụng các luồng bổ sung.