Java ThreadPoolExecutor: Cập nhật kích thước nhóm lõi một cách linh hoạt từ chối các tác vụ đến không liên tục


13

Tôi đang gặp phải một vấn đề trong đó nếu tôi cố gắng thay đổi kích thước nhóm ThreadPoolExecutorlõi của một số khác sau khi nhóm được tạo, sau đó không liên tục, một số tác vụ bị từ chối RejectedExecutionExceptionmặc dù tôi không bao giờ gửi nhiều hơn queueSize + maxPoolSizesố lượng tác vụ.

Vấn đề mà tôi đang cố gắng giải quyết là mở rộng ThreadPoolExecutorthay đổi kích thước các luồng cốt lõi của nó dựa trên các lệnh thực thi đang chờ xử lý trong hàng đợi của nhóm luồng. Tôi cần điều này bởi vì theo mặc định, a ThreadPoolExecutorsẽ tạo một cái mới Threadchỉ khi hàng đợi đầy.

Đây là một chương trình Pure Java 8 độc lập nhỏ thể hiện vấn đề.

import static java.lang.Math.max;
import static java.lang.Math.min;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolResizeTest {

    public static void main(String[] args) throws Exception {
        // increase the number of iterations if unable to reproduce
        // for me 100 iterations have been enough
        int numberOfExecutions = 100;

        for (int i = 1; i <= numberOfExecutions; i++) {
            executeOnce();
        }
    }

    private static void executeOnce() throws Exception {
        int minThreads = 1;
        int maxThreads = 5;
        int queueCapacity = 10;

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                minThreads, maxThreads,
                0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(queueCapacity),
                new ThreadPoolExecutor.AbortPolicy()
        );

        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(() -> resizeThreadPool(pool, minThreads, maxThreads),
                0, 10, TimeUnit.MILLISECONDS);
        CompletableFuture<Void> taskBlocker = new CompletableFuture<>();

        try {
            int totalTasksToSubmit = queueCapacity + maxThreads;

            for (int i = 1; i <= totalTasksToSubmit; i++) {
                // following line sometimes throws a RejectedExecutionException
                pool.submit(() -> {
                    // block the thread and prevent it from completing the task
                    taskBlocker.join();
                });
                // Thread.sleep(10); //enabling even a small sleep makes the problem go away
            }
        } finally {
            taskBlocker.complete(null);
            scheduler.shutdown();
            pool.shutdown();
        }
    }

    /**
     * Resize the thread pool if the number of pending tasks are non-zero.
     */
    private static void resizeThreadPool(ThreadPoolExecutor pool, int minThreads, int maxThreads) {
        int pendingExecutions = pool.getQueue().size();
        int approximateRunningExecutions = pool.getActiveCount();

        /*
         * New core thread count should be the sum of pending and currently executing tasks
         * with an upper bound of maxThreads and a lower bound of minThreads.
         */
        int newThreadCount = min(maxThreads, max(minThreads, pendingExecutions + approximateRunningExecutions));

        pool.setCorePoolSize(newThreadCount);
        pool.prestartAllCoreThreads();
    }
}

Tại sao pool nên ném RejectionExecutException nếu tôi không bao giờ gửi thêm rằng queueCapacity + maxThreads. Tôi không bao giờ thay đổi các chủ đề tối đa vì vậy theo định nghĩa của ThreadPoolExecutor, nó sẽ phù hợp với nhiệm vụ trong một Chủ đề hoặc cho hàng đợi.

Tất nhiên, nếu tôi không bao giờ thay đổi kích thước nhóm, thì nhóm luồng không bao giờ từ chối bất kỳ nội dung gửi nào. Điều này cũng khó gỡ lỗi vì việc thêm bất kỳ sự chậm trễ nào trong bài nộp khiến vấn đề không còn nữa.

Bất kỳ con trỏ nào về cách sửa lỗi RejectionExecutException?


Tại sao không cung cấp triển khai của riêng bạn ExecutorServicebằng cách gói một cái hiện có, trong đó gửi lại các tác vụ không gửi được do thay đổi kích thước?
daniu

@daniu đó là một cách giải quyết. Điểm quan trọng của câu hỏi là tại sao pool nên ném RejectionExecutException nếu tôi không bao giờ gửi thêm rằng queueCapacity + maxThreads. Tôi không bao giờ thay đổi các chủ đề tối đa vì vậy theo định nghĩa của ThreadPoolExecutor, nó sẽ phù hợp với nhiệm vụ trong một Chủ đề hoặc cho hàng đợi.
Swaranga Sarma

Ok tôi dường như đã hiểu nhầm câu hỏi của bạn. Nó là gì? Bạn có muốn biết lý do tại sao hành vi xảy ra hoặc làm thế nào bạn có xung quanh nó gây ra vấn đề cho bạn?
daniu

Có, việc thay đổi triển khai của tôi thành dịch vụ thực thi là không khả thi vì rất nhiều mã đề cập đến ThreadPoolExecutor. Vì vậy, nếu tôi vẫn muốn có một ThreadPoolExecutor có thể thay đổi kích thước, tôi cần biết làm thế nào tôi có thể sửa nó. Có thể cách đúng để làm điều gì đó như thế này là mở rộng ThreadPoolExecutor và có quyền truy cập vào một số biến được bảo vệ của nó và cập nhật kích thước nhóm trong một khối được đồng bộ hóa trên một khóa được chia sẻ bởi siêu hạng.
Swaranga Sarma

Mở rộng ThreadPoolExecutorrất có thể là một ý tưởng tồi và bạn cũng không cần phải thay đổi mã hiện có trong trường hợp này chứ? Tốt nhất bạn nên cung cấp một số ví dụ về cách mã thực tế của bạn truy cập vào người thi hành. Tôi sẽ ngạc nhiên nếu nó sử dụng nhiều phương thức cụ thể ThreadPoolExecutor(không phải trong ExecutorService).
daniu

Câu trả lời:


5

Đây là một kịch bản tại sao điều này xảy ra:

Trong ví dụ của tôi, tôi sử dụng minThreads = 0, maxThreads = 2 và queueCapacity = 2 để làm cho nó ngắn hơn. Lệnh đầu tiên được gửi, điều này được thực hiện trong phương thức thực thi:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

đối với lệnh này workQueue.offer (lệnh) hơn addWorker (null, false) được thực thi. Đầu tiên, luồng công nhân đưa lệnh này ra khỏi hàng đợi trong phương thức chạy luồng, vì vậy tại thời điểm này, hàng đợi vẫn còn một lệnh,

Lệnh thứ hai được gửi lần này workQueue.offer (lệnh) được thực thi. Bây giờ hàng đợi đã đầy

Bây giờ, lên lịchExecutorService thực thi phương thức resizeThreadPool gọi setCorePoolSize bằng maxThreads. Đây là phương thức setCorePoolSize:

 public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

Phương thức này thêm một công nhân sử dụng addWorker (null, true). Không có 2 hàng công nhân đang chạy, tối đa và hàng đợi đã đầy.

Lệnh thứ ba được gửi và thất bại vì workQueue.offer (lệnh) và addWorker (lệnh, false) không thành công, dẫn đến Ngoại lệ:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@24c22fe rejected from java.util.concurrent.ThreadPoolExecutor@cd1e646[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at ThreadPoolResizeTest.executeOnce(ThreadPoolResizeTest.java:60)
at ThreadPoolResizeTest.runTest(ThreadPoolResizeTest.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:365)

Tôi nghĩ để giải quyết vấn đề này, bạn nên đặt dung lượng của hàng đợi thành tối đa các lệnh bạn muốn thực thi.


Chính xác. Tôi đã có thể repro bằng cách sao chép mã vào lớp của riêng tôi và thêm loggers. Về cơ bản, khi hàng đợi đầy, và tôi gửi một nhiệm vụ mới, nó sẽ cố gắng tạo một Công nhân mới. Trong khi đó, tại thời điểm đó, trình khôi phục của tôi cũng gọi setCorePoolSize thành 2, điều đó cũng tạo ra một Công nhân mới. Tại thời điểm này, hai Công nhân đang cạnh tranh để được thêm nhưng cả hai đều không thể vì điều đó sẽ vi phạm ràng buộc kích thước nhóm tối đa nên việc gửi nhiệm vụ mới bị từ chối. Tôi nghĩ rằng đây là một điều kiện cuộc đua và tôi đã nộp báo cáo lỗi cho OpenJDK. Hãy xem nào. Nhưng bạn đã trả lời câu hỏi của tôi để bạn nhận được tiền thưởng. Cảm ơn bạn.
Swaranga Sarma

2

Không chắc chắn nếu điều này đủ điều kiện là lỗi. Đây là hành vi khi các luồng công nhân bổ sung được tạo sau khi hàng đợi đầy nhưng điều này đã được ghi chú trong các tài liệu java mà người gọi phải xử lý các tác vụ bị từ chối.

Tài liệu Java

Nhà máy cho chủ đề mới. Tất cả các luồng được tạo bằng cách sử dụng nhà máy này (thông qua phương thức addWorker). Tất cả người gọi phải được chuẩn bị để addWorker không thành công, điều này có thể phản ánh chính sách của hệ thống hoặc người dùng giới hạn số lượng luồng. Mặc dù nó không được coi là một lỗi, nhưng việc không tạo các luồng có thể dẫn đến các tác vụ mới bị từ chối hoặc các tác vụ hiện có bị kẹt trong hàng đợi.

Khi bạn thay đổi kích thước kích thước nhóm lõi, giả sử tăng, các công nhân bổ sung được tạo ( addWorkerphương thức trong setCorePoolSize) và lệnh gọi để tạo công việc bổ sung ( addWorkerphương thức từ execute) bị từ chối khi addWorkertrả về false ( add Workerđoạn mã cuối) vì đã có đủ công nhân bổ sung được tạo bởi setCorePoolSize nhưng chưa chạy để phản ánh cập nhật trong hàng đợi .

Các bộ phận liên quan

Đối chiếu

public void setCorePoolSize(int corePoolSize) {
    ....
    int k = Math.min(delta, workQueue.size());
    while (k-- > 0 && addWorker(null, true)) {
        if (workQueue.isEmpty())
             break;
    }
}

public void execute(Runnable command) {
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

private boolean addWorker(Runnable firstTask, boolean core) {
....
   if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
     return false;             
}

Sử dụng trình xử lý thực thi từ chối thử lại tùy chỉnh (Điều này sẽ hoạt động cho trường hợp của bạn vì bạn có giới hạn trên là kích thước nhóm tối đa). Hãy điều chỉnh khi cần thiết.

public static class RetryRejectionPolicy implements RejectedExecutionHandler {
    public RetryRejectionPolicy () {}

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
           while(true)
            if(e.getQueue().offer(r)) break;
        }
    }
}

ThreadPoolExecutor pool = new ThreadPoolExecutor(
      minThreads, maxThreads,
      0, TimeUnit.SECONDS,
      new LinkedBlockingQueue<Runnable>(queueCapacity),
      new ThreadPoolResizeTest.RetryRejectionPolicy()
 );

Cũng lưu ý rằng việc sử dụng tắt máy của bạn là không chính xác vì điều này sẽ không chờ tác vụ được gửi để hoàn tất thực hiện mà awaitTerminationthay vào đó sử dụng .


Tôi nghĩ rằng tắt máy chờ các tác vụ đã được gửi, theo JavaDoc: shutdown () Thực hiện tắt máy có trật tự trong đó các tác vụ được gửi trước đó được thực thi, nhưng không có tác vụ mới nào được chấp nhận.
Thomas Krieger

@ThomasKrieger - Nó sẽ thực hiện các nhiệm vụ đã nộp nhưng sẽ không chờ đợi họ để kết thúc - từ tài liệu docs.oracle.com/javase/7/docs/api/java/util/concurrent/... - Phương pháp này không chờ nộp trước nhiệm vụ hoàn thành thực hiện. Sử dụng awaitTermination để làm điều đó.
Sagar Veeram
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.