Java ThreadPoolExecutor: การอัปเดตขนาดพูหลักปฏิเสธงานที่เข้ามาเป็นระยะ ๆ แบบไดนามิก


13

ฉันกำลังประสบปัญหาซึ่งถ้าฉันพยายามปรับขนาดThreadPoolExecutorแกนกลางของสระว่ายน้ำให้เป็นจำนวนที่แตกต่างกันหลังจากที่สร้างกลุ่มนั้นเป็นระยะ ๆ งานบางอย่างถูกปฏิเสธด้วยงานRejectedExecutionExceptionแม้ว่าฉันจะไม่ส่งงานมากกว่าqueueSize + maxPoolSizeจำนวน

ปัญหาที่ฉันพยายามแก้ไขคือการขยายThreadPoolExecutorที่ปรับขนาดเธรดหลักตามการดำเนินการที่ค้างอยู่ซึ่งอยู่ในคิวของเธรดพูล ฉันต้องการสิ่งนี้เพราะโดยปกติแล้ว a ThreadPoolExecutorจะสร้างใหม่Threadเฉพาะเมื่อคิวเต็ม

นี่คือโปรแกรม Pure Java 8 ขนาดเล็กที่มีในตัวเองซึ่งแสดงให้เห็นถึงปัญหา

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();
    }
}

เหตุใดพูลจึงควรโยน RejectedExecutionException หากฉันไม่เคยส่งเพิ่มเติมว่า queueCapacity + maxThreads ฉันไม่เคยเปลี่ยนเธรดสูงสุดดังนั้นตามคำจำกัดความของ ThreadPoolExecutor มันควรรองรับงานในเธรดหรือคิว

แน่นอนถ้าฉันไม่เคยปรับขนาดพูลสระเธรดจะไม่ปฏิเสธการส่งใด ๆ นอกจากนี้ยังเป็นเรื่องยากที่จะแก้ปัญหาตั้งแต่การเพิ่มความล่าช้าใด ๆ ในการส่งทำให้ปัญหาหายไป

ตัวชี้ใด ๆ เกี่ยวกับวิธีแก้ไข RejectedExecutionException


ทำไมไม่จัดให้มีการติดตั้งของคุณเองExecutorServiceโดยห่อสิ่งที่มีอยู่ซึ่งส่งงานที่ล้มเหลวในการส่งอีกครั้งเนื่องจากการปรับขนาด?
daniu

@daniu นั่นเป็นวิธีแก้ปัญหา ประเด็นของคำถามคือทำไมกลุ่มที่ควรโยน RejectedExecutionException ถ้าฉันไม่เคยส่งมากกว่านั้น QueCapacity + maxThreads ฉันไม่เคยเปลี่ยนเธรดสูงสุดดังนั้นตามคำจำกัดความของ ThreadPoolExecutor มันควรรองรับงานในเธรดหรือคิว
Swaranga Sarma

ตกลงฉันดูเหมือนจะเข้าใจคำถามของคุณผิด มันคืออะไร? คุณต้องการที่จะรู้ว่าทำไมพฤติกรรมถึงเกิดขึ้นหรือวิธีการที่คุณทำมันทำให้เกิดปัญหากับคุณ?
daniu

ใช่การเปลี่ยนการใช้งานของฉันเป็นบริการผู้ปฏิบัติการนั้นไม่สามารถทำได้เนื่องจากรหัสจำนวนมากอ้างถึง ThreadPoolExecutor ดังนั้นหากฉันยังต้องการที่จะมี ThreadPoolExecutor ที่ปรับขนาดได้ฉันต้องรู้ว่าฉันจะแก้ไขได้อย่างไร อาจเป็นวิธีที่ถูกต้องในการทำสิ่งนี้เช่นการขยาย ThreadPoolExecutor และเข้าถึงตัวแปรที่ได้รับการป้องกันบางส่วนและอัพเดตขนาดพูลภายในบล็อกที่ซิงโครไนซ์บนล็อคที่แชร์โดยซุปเปอร์คลาส
Swaranga Sarma

การขยายThreadPoolExecutorอาจเป็นแนวคิดที่ไม่ดีและคุณไม่จำเป็นต้องเปลี่ยนรหัสที่มีอยู่ในกรณีนี้ด้วยใช่ไหม มันจะเป็นการดีที่สุดถ้าคุณให้ตัวอย่างว่าโค้ดจริงของคุณเข้าถึงผู้ดำเนินการ ฉันจะแปลกใจถ้ามันใช้หลายวิธีเฉพาะThreadPoolExecutor(เช่นไม่ได้อยู่ในExecutorService)
daniu

คำตอบ:


5

นี่คือสถานการณ์ที่ทำไมสิ่งนี้จึงเกิดขึ้น:

ในตัวอย่างของฉันฉันใช้ minThreads = 0, maxThreads = 2 และ queueCapacity = 2 เพื่อทำให้สั้นลง คำสั่งแรกที่ได้รับการส่งซึ่งจะทำในวิธีการดำเนินการ:

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);
}

สำหรับคำสั่งนี้จะใช้งาน workQueue.offer (คำสั่ง) มากกว่า addWorker (null, false) เธรดผู้ปฏิบัติงานก่อนใช้คำสั่งนี้ออกจากคิวในวิธีการรันเธรดดังนั้นในขณะนี้คิวยังคงมีหนึ่งคำสั่ง

คำสั่งที่สองได้รับการส่งครั้งนี้ workQueue.offer (คำสั่ง) จะถูกดำเนินการ ตอนนี้คิวเต็มแล้ว

ตอนนี้ ScheduledExecutorService จะเรียกใช้เมธอด resizeThreadPool ซึ่งเรียกใช้ setCorePoolSize ด้วย maxThreads นี่คือวิธี 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;
        }
    }
}

วิธีนี้จะเพิ่มผู้ปฏิบัติงานหนึ่งคนโดยใช้ addWorker (null, จริง) ไม่มีคิวผู้ทำงาน 2 คนกำลังทำงานสูงสุดและคิวเต็ม

คำสั่งที่สามรับการส่งและล้มเหลวเนื่องจาก workQueue.offer (คำสั่ง) และ addWorker (คำสั่งเท็จ) ล้มเหลวนำไปสู่ข้อยกเว้น:

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)

ฉันคิดว่าการแก้ปัญหานี้คุณควรกำหนดความจุของคิวให้สูงสุดของคำสั่งที่คุณต้องการดำเนินการ


แก้ไข. ฉันสามารถทำซ้ำได้โดยการคัดลอกรหัสไปยังชั้นเรียนของฉันและเพิ่มตัวบันทึก โดยทั่วไปเมื่อคิวเต็มและฉันส่งงานใหม่มันจะพยายามสร้าง Worker ใหม่ ในขณะเดียวกันถ้าในขณะนั้นผู้ปรับแต่งของฉันเรียก setCorePoolSize เป็น 2 ซึ่งจะสร้างผู้ทำงานใหม่ขึ้นมา ณ จุดนี้คนงานสองคนกำลังแข่งขันกันที่จะเพิ่ม แต่พวกเขาทั้งคู่ไม่สามารถทำได้เพราะจะเป็นการละเมิดข้อ จำกัด ขนาดสูงสุดของพูลดังนั้นการส่งงานใหม่จะถูกปฏิเสธ ฉันคิดว่านี่เป็นเงื่อนไขการแข่งขันและฉันได้ยื่นรายงานข้อผิดพลาดไปยัง OpenJDK มาดูกัน. แต่คุณตอบคำถามของฉันเพื่อให้คุณได้รับรางวัล ขอบคุณ.
Swaranga Sarma

2

ไม่แน่ใจว่านี่เป็นข้อผิดพลาดหรือไม่ นี่คือพฤติกรรมเมื่อมีการสร้างเธรดตัวทำงานเพิ่มเติมหลังจากคิวเต็ม แต่สิ่งนี้ได้รับการบันทึกไว้ในเอกสารจาวาที่ผู้เรียกต้องจัดการกับงานที่ถูกปฏิเสธ

เอกสาร Java

โรงงานสำหรับหัวข้อใหม่ กระทู้ทั้งหมดถูกสร้างขึ้นโดยใช้โรงงานนี้ (ผ่านวิธี addWorker) ผู้โทรทุกคนจะต้องเตรียมพร้อมสำหรับการ addWorker ที่จะล้มเหลวซึ่งอาจสะท้อนถึงระบบหรือนโยบายของผู้ใช้ที่ จำกัด จำนวนเธรด แม้ว่าจะไม่ถือว่าเป็นข้อผิดพลาดความล้มเหลวในการสร้างเธรดอาจส่งผลให้งานใหม่ถูกปฏิเสธหรือมีอยู่แล้วที่เหลืออยู่ในคิว

เมื่อคุณปรับขนาดขนาดพูลหลักให้เพิ่มขนาดขึ้นระบบจะสร้างคนงานเพิ่มเติม ( addWorkerวิธีการsetCorePoolSize) และการโทรเพื่อสร้างงานเพิ่มเติม ( addWorkerวิธีการจากexecute) ถูกปฏิเสธเมื่อaddWorkerผลตอบแทนเท็จ ( add Workerตัวอย่างรหัสสุดท้าย) เนื่องจากมีพนักงานเพิ่มเติมเพียงพออยู่แล้ว ที่สร้างขึ้นโดยแต่ไม่ทำงานยังไม่ได้สะท้อนให้เห็นถึงการปรับปรุงในคิวsetCorePoolSize

ชิ้นส่วนที่เกี่ยวข้อง

เปรียบเทียบ

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;             
}

ใช้ตัวจัดการการดำเนินการปฏิเสธการลองใหม่แบบกำหนดเอง (สิ่งนี้ควรใช้กับกรณีของคุณเนื่องจากคุณมีขอบบนเป็นขนาดพูลสูงสุด) โปรดปรับได้ตามต้องการ

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()
 );

นอกจากนี้โปรดทราบว่าการใช้งานการปิดระบบของคุณไม่ถูกต้องเนื่องจากจะไม่รอให้งานที่ส่งไปดำเนินการเสร็จสมบูรณ์ แต่ใช้awaitTerminationแทน


ฉันคิดว่าการปิดระบบรอสำหรับงานที่ส่งไปแล้วตาม JavaDoc: shutdown () เริ่มต้นการปิดอย่างเป็นระเบียบซึ่งงานที่ส่งก่อนหน้านี้จะถูกดำเนินการ แต่จะไม่มีงานใหม่ที่จะยอมรับ
โทมัส Krieger

@ThomasKrieger - มันจะดำเนินการงานที่ส่งไปแล้ว แต่จะไม่รอให้เสร็จ - จาก docs docs.oracle.com/javase/7/docs/api/java/util/concurrent/วิธีนี้ไม่รอให้ส่งมาก่อนหน้านี้ งานที่จะดำเนินการให้เสร็จสมบูรณ์ ใช้ awaitTermination เพื่อทำเช่นนั้น
Sagar Veeram
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.