Làm cách nào tôi có thể khắc phục hạn chế này ở ThreadPoolExecutor
nơi hàng đợi cần được giới hạn và đầy trước khi bắt đầu nhiều luồng hơn.
Tôi tin rằng cuối cùng tôi đã tìm thấy một giải pháp hơi thanh lịch (có thể là một chút hacky) cho hạn chế này với ThreadPoolExecutor
. Nó liên quan đến việc mở rộng LinkedBlockingQueue
để có nó trở lại false
cho queue.offer(...)
khi đã có một số nhiệm vụ xếp hàng đợi. Nếu các luồng hiện tại không theo kịp các tác vụ được xếp hàng đợi, TPE sẽ thêm các luồng bổ sung. Nếu nhóm đã ở mức tối đa luồng, thì nhóm RejectedExecutionHandler
sẽ được gọi. Nó là trình xử lý sau đó thực hiện put(...)
vào hàng đợi.
Chắc chắn là kỳ lạ khi viết một hàng đợi offer(...)
có thể trả về false
và put()
không bao giờ chặn, vì vậy đó là phần hack. Nhưng điều này hoạt động tốt với việc sử dụng hàng đợi của TPE nên tôi không thấy có vấn đề gì khi thực hiện việc này.
Đây là mã:
// extend LinkedBlockingQueue to force offer() to return false conditionally
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
private static final long serialVersionUID = -6903933921423432194L;
@Override
public boolean offer(Runnable e) {
// Offer it to the queue if there is 0 items already queued, else
// return false so the TPE will add another thread. If we return false
// and max threads have been reached then the RejectedExecutionHandler
// will be called which will do the put into the queue.
if (size() == 0) {
return super.offer(e);
} else {
return false;
}
}
};
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1 /*core*/, 50 /*max*/,
60 /*secs*/, TimeUnit.SECONDS, queue);
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
// This does the actual put into the queue. Once the max threads
// have been reached, the tasks will then queue up.
executor.getQueue().put(r);
// we do this after the put() to stop race conditions
if (executor.isShutdown()) {
throw new RejectedExecutionException(
"Task " + r + " rejected from " + e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
});
Với cơ chế này, khi tôi gửi nhiệm vụ vào hàng đợi, ý ThreadPoolExecutor
muốn:
- Chia tỷ lệ số luồng lên đến kích thước lõi ban đầu (ở đây 1).
- Cung cấp nó cho hàng đợi. Nếu hàng đợi trống, nó sẽ được xếp hàng để được xử lý bởi các luồng hiện có.
- Nếu hàng đợi đã có 1 hoặc nhiều phần tử, giá trị
offer(...)
sẽ trả về false.
- Nếu trả về false, hãy mở rộng số luồng trong nhóm cho đến khi chúng đạt đến số lượng tối đa (ở đây là 50).
- Nếu ở mức tối đa thì nó gọi
RejectedExecutionHandler
- Sau
RejectedExecutionHandler
đó, đặt nhiệm vụ vào hàng đợi để xử lý bởi luồng có sẵn đầu tiên theo thứ tự FIFO.
Mặc dù trong đoạn mã ví dụ của tôi ở trên, hàng đợi không bị ràng buộc, bạn cũng có thể định nghĩa nó là hàng đợi có giới hạn. Ví dụ: nếu bạn thêm dung lượng 1000 vào LinkedBlockingQueue
thì nó sẽ:
- mở rộng các chủ đề lên đến tối đa
- sau đó xếp hàng cho đến khi đầy với 1000 nhiệm vụ
- sau đó chặn người gọi cho đến khi có dung lượng cho hàng đợi.
Ngoài ra, nếu bạn cần sử dụng offer(...)
trong thời gian
RejectedExecutionHandler
đó, bạn có thể sử dụng offer(E, long, TimeUnit)
phương pháp này thay vì Long.MAX_VALUE
thời gian chờ.
Cảnh báo:
Nếu bạn mong đợi các tác vụ được thêm vào trình thực thi sau khi nó đã tắt, thì bạn có thể muốn thông minh hơn về việc RejectedExecutionException
loại bỏ tùy chỉnh của chúng tôi RejectedExecutionHandler
khi dịch vụ thực thi đã bị tắt. Cảm ơn @RaduToader đã chỉ ra điều này.
Biên tập:
Một tinh chỉnh khác cho câu trả lời này có thể là hỏi TPE nếu có các chuỗi không hoạt động và chỉ xếp hạng mục nếu có. Bạn sẽ phải tạo một lớp thực sự cho điều này và thêmourQueue.setThreadPoolExecutor(tpe);
phương thức vào nó.
Sau đó, offer(...)
phương pháp của bạn có thể trông giống như sau:
- Kiểm tra xem nếu
tpe.getPoolSize() == tpe.getMaximumPoolSize()
trường hợp đó chỉ cần gọisuper.offer(...)
.
- Khác nếu
tpe.getPoolSize() > tpe.getActiveCount()
sau đó gọisuper.offer(...)
vì dường như có chủ đề nhàn rỗi.
- Nếu không, hãy quay lại
false
fork một chủ đề khác.
Có lẽ điều này:
int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
return super.offer(e);
} else {
return false;
}
Lưu ý rằng các phương thức get trên TPE rất tốn kém vì chúng truy cập volatile
các trường hoặc (trong trường hợp getActiveCount()
) khóa TPE và xem danh sách luồng. Ngoài ra, có các điều kiện chạy đua ở đây có thể khiến một tác vụ được xếp hàng không đúng cách hoặc một luồng khác được phân nhánh khi có một luồng không hoạt động.