Lớp sau bao bọc xung quanh ThreadPoolExecutor và sử dụng Semaphore để chặn sau đó hàng đợi công việc đầy:
public final class BlockingExecutor {
private final Executor executor;
private final Semaphore semaphore;
public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
this.semaphore = new Semaphore(queueSize + maxPoolSize);
}
private void execImpl (final Runnable command) throws InterruptedException {
semaphore.acquire();
try {
executor.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
// will never be thrown with an unbounded buffer (LinkedBlockingQueue)
semaphore.release();
throw e;
}
}
public void execute (Runnable command) throws InterruptedException {
execImpl(command);
}
}
Lớp wrapper này dựa trên một giải pháp được đưa ra trong cuốn sách Java Concurrency in Practice của Brian Goetz. Lời giải trong cuốn sách chỉ nhận hai tham số hàm tạo: an Executor
và một giới hạn được sử dụng cho semaphore. Điều này được thể hiện trong câu trả lời do Fixpoint đưa ra. Có một vấn đề với cách tiếp cận đó: nó có thể rơi vào trạng thái mà các luồng hồ bơi đang bận, hàng đợi đã đầy, nhưng semaphore vừa phát hành giấy phép. ( semaphore.release()
trong khối cuối cùng). Ở trạng thái này, một nhiệm vụ mới có thể lấy giấy phép vừa phát hành, nhưng bị từ chối vì hàng đợi nhiệm vụ đã đầy. Tất nhiên đây không phải là điều bạn muốn; bạn muốn chặn trong trường hợp này.
Để giải quyết điều này, chúng ta phải sử dụng một hàng đợi không bị ràng buộc , như JCiP đã đề cập rõ ràng. Semaphore hoạt động như một người bảo vệ, tạo ra hiệu ứng của kích thước hàng đợi ảo. Điều này có tác dụng phụ là đơn vị có thể chứa các maxPoolSize + virtualQueueSize + maxPoolSize
nhiệm vụ. Tại sao vậy? Bởi vì
semaphore.release()
trong khối cuối cùng. Nếu tất cả các luồng nhóm gọi câu lệnh này cùng một lúc, thì các maxPoolSize
giấy phép sẽ được giải phóng, cho phép cùng một số tác vụ vào đơn vị. Nếu chúng ta đang sử dụng một hàng đợi bị giới hạn, nó sẽ vẫn đầy, dẫn đến nhiệm vụ bị từ chối. Bây giờ, bởi vì chúng ta biết rằng điều này chỉ xảy ra khi một luồng hồ bơi gần như hoàn thành, nên đây không phải là vấn đề. Chúng tôi biết rằng luồng hồ bơi sẽ không chặn, vì vậy một tác vụ sẽ sớm được thực hiện từ hàng đợi.
Tuy nhiên, bạn có thể sử dụng một hàng đợi có giới hạn. Chỉ cần đảm bảo rằng kích thước của nó bằng nhau virtualQueueSize + maxPoolSize
. Kích thước lớn hơn là vô dụng, semaphore sẽ ngăn không cho nhiều mục hơn vào. Kích thước nhỏ hơn sẽ dẫn đến các tác vụ bị từ chối. Cơ hội các nhiệm vụ bị từ chối tăng lên khi kích thước giảm. Ví dụ: giả sử bạn muốn một trình thực thi có giới hạn với maxPoolSize = 2 và virtualQueueSize = 5. Sau đó, lấy một semaphore với 5 + 2 = 7 giấy phép và kích thước hàng đợi thực tế là 5 + 2 = 7. Số nhiệm vụ thực có thể có trong đơn vị khi đó là 2 + 5 + 2 = 9. Khi trình thực thi đã đầy (5 tác vụ trong hàng đợi, 2 trong nhóm luồng, do đó 0 giấy phép khả dụng) và TẤT CẢ các luồng nhóm giải phóng giấy phép của chúng, thì chính xác 2 giấy phép có thể được thực hiện bởi các tác vụ đến.
Giờ đây, giải pháp từ JCiP hơi cồng kềnh để sử dụng vì nó không thực thi tất cả các ràng buộc này (hàng đợi không bị ràng buộc hoặc bị ràng buộc với các hạn chế toán học đó, v.v.). Tôi nghĩ rằng đây chỉ là một ví dụ điển hình để chứng minh cách bạn có thể xây dựng các lớp an toàn luồng mới dựa trên các phần đã có sẵn, nhưng không phải là một lớp hoàn chỉnh, có thể tái sử dụng. Tôi không nghĩ rằng cái sau là chủ ý của tác giả.