จะทำให้ ThreadPoolExecutor เพิ่มเธรดให้สูงสุดก่อนเข้าคิวได้อย่างไร


102

ฉันรู้สึกท้อแท้มาระยะหนึ่งแล้วกับพฤติกรรมเริ่มต้นThreadPoolExecutorซึ่งอยู่เบื้องหลังExecutorServiceเธรดพูลที่พวกเราหลายคนใช้ หากต้องการอ้างอิงจาก Javadocs:

หากมีมากกว่า corePoolSize แต่น้อยกว่าหัวข้อ maximumPoolSize ทำงานหัวข้อใหม่จะถูกสร้างขึ้นเฉพาะในกรณีที่คิวเต็ม

สิ่งนี้หมายความว่าถ้าคุณกำหนดเธรดพูลด้วยรหัสต่อไปนี้เธรดจะไม่เริ่มเธรดที่ 2 เนื่องจากLinkedBlockingQueueไม่ถูกผูกไว้

ExecutorService threadPool =
   new ThreadPoolExecutor(1 /*core*/, 50 /*max*/, 60 /*timeout*/,
      TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(/* unlimited queue */));

เฉพาะในกรณีที่คุณมีคิวที่ถูกล้อมรอบและคิวเต็มเท่านั้นที่จะมีเธรดที่อยู่เหนือหมายเลขหลักเริ่มต้น ฉันสงสัยว่าโปรแกรมเมอร์มัลติเธรด Java รุ่นจูเนียร์จำนวนมากไม่ทราบถึงพฤติกรรมนี้ของไฟล์ThreadPoolExecutor.

ตอนนี้ฉันมีกรณีการใช้งานที่เฉพาะเจาะจงซึ่งไม่เหมาะสม ฉันกำลังมองหาวิธีต่างๆโดยไม่ต้องเขียนคลาส TPE ของตัวเองเพื่อแก้ไข

ข้อกำหนดของฉันมีไว้สำหรับบริการเว็บที่โทรกลับไปยังบุคคลที่สามที่อาจไม่น่าเชื่อถือ

  • ฉันไม่ต้องการโทรกลับพร้อมกันกับคำขอทางเว็บดังนั้นฉันต้องการใช้เธรดพูล
  • ฉันมักจะได้เวลาสองสามนาทีนี้ดังนั้นฉันจึงไม่อยากมีnewFixedThreadPool(...)เธรดจำนวนมากที่ส่วนใหญ่อยู่เฉยๆ
  • บ่อยครั้งที่ฉันได้รับปริมาณการใช้งานจำนวนมากและฉันต้องการเพิ่มจำนวนเธรดให้เป็นค่าสูงสุด (สมมติว่า 50)
  • ฉันต้องพยายามอย่างเต็มที่เพื่อทำการโทรกลับทั้งหมดดังนั้นฉันจึงต้องการจัดคิวรายการเพิ่มเติมที่สูงกว่า 50 ฉันไม่ต้องการทำให้เว็บเซิร์ฟเวอร์ส่วนที่เหลือของฉันล้นโดยใช้ไฟล์newCachedThreadPool().

ฉันจะแก้ไขข้อ จำกัด นี้ได้อย่างไรในThreadPoolExecutorกรณีที่คิวต้องถูกล้อมรอบและเต็มก่อนที่จะเริ่มเธรดเพิ่มเติม ฉันจะเริ่มเธรดเพิ่มเติมก่อนเข้าคิวงานได้อย่างไร

แก้ไข:

@Flavio เป็นจุดที่ดีเกี่ยวกับการใช้ThreadPoolExecutor.allowCoreThreadTimeOut(true)เพื่อให้เธรดหลักหมดเวลาและออก ฉันพิจารณาแล้ว แต่ฉันยังต้องการคุณสมบัติแกนเธรด ฉันไม่ต้องการให้จำนวนเธรดในพูลลดลงต่ำกว่าขนาดแกนกลางถ้าเป็นไปได้


1
เนื่องจากตัวอย่างของคุณสร้างเธรดสูงสุด 10 เธรดมีการประหยัดจริงหรือไม่ในการใช้สิ่งที่ขยาย / ลดขนาดมากกว่าเธรดพูลขนาดคงที่?
bstempi

จุดดี @bstempi จำนวนเป็นไปตามอำเภอใจ ฉันเพิ่มมันในคำถามเป็น 50 ไม่แน่ใจว่ามีเธรดพร้อมกันกี่เธรดที่ฉันต้องการใช้งานได้จริงตอนนี้ฉันมีวิธีแก้ปัญหานี้แล้ว
สีเทา

1
โอ้ย! 10 โหวตถ้าฉันทำได้ที่นี่ตำแหน่งเดียวกับที่ฉันอยู่
Eugene

คำตอบ:


52

ฉันจะแก้ไขข้อ จำกัด นี้ได้อย่างไรในThreadPoolExecutorกรณีที่คิวต้องถูกล้อมรอบและเต็มก่อนที่จะเริ่มเธรดเพิ่มเติม

ฉันเชื่อว่าฉันได้พบในที่สุดก็ค่อนข้างสง่า (อาจจะเป็น hacky เล็ก ๆ น้อย ๆ ) วิธีการแก้ปัญหาข้อ จำกัด ThreadPoolExecutorนี้ด้วย มันเกี่ยวข้องกับการขยายLinkedBlockingQueueที่จะมีมันกลับfalseสำหรับqueue.offer(...)เมื่อมีอยู่แล้วบางงานจัดคิว หากเธรดปัจจุบันไม่สอดคล้องกับงานที่อยู่ในคิว TPE จะเพิ่มเธรดเพิ่มเติม หากพูลอยู่ที่เธรดสูงสุดแล้วระบบRejectedExecutionHandlerจะเรียกใช้ เป็นตัวจัดการที่จะput(...)เข้าสู่คิว

เป็นเรื่องแปลกที่จะเขียนคิวที่offer(...)สามารถย้อนกลับได้falseและput()ไม่บล็อกนั่นคือส่วนที่แฮ็ก แต่ใช้ได้ดีกับการใช้คิวของ TPE ดังนั้นฉันจึงไม่เห็นปัญหาในการทำเช่นนี้

นี่คือรหัส:

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

ด้วยกลไกนี้เมื่อฉันส่งงานไปยังคิวThreadPoolExecutorพินัยกรรม:

  1. ปรับขนาดจำนวนเธรดให้เป็นขนาดแกนเริ่มต้น (ที่นี่ 1)
  2. เสนอให้ตามคิว หากคิวว่างจะถูกจัดคิวให้จัดการโดยเธรดที่มีอยู่
  3. ถ้าคิวมี 1 องค์ประกอบหรือมากกว่าอยู่แล้วoffer(...)จะส่งคืนเท็จ
  4. หากส่งคืนค่าเท็จให้ปรับขนาดจำนวนเธรดในพูลจนกว่าจะถึงจำนวนสูงสุด (ที่นี่ 50)
  5. ถ้าสูงสุดก็จะเรียกไฟล์ RejectedExecutionHandler
  6. RejectedExecutionHandlerแล้วทำให้งานลงในคิวที่จะประมวลผลโดยหัวข้อแรกในการสั่งซื้อ FIFO

แม้ว่าในโค้ดตัวอย่างของฉันด้านบนคิวจะไม่ถูกผูกไว้ แต่คุณสามารถกำหนดเป็นคิวที่มีขอบเขตได้ ตัวอย่างเช่นหากคุณเพิ่มความจุ 1,000 เข้าไปในLinkedBlockingQueueนั้นมันจะ:

  1. ปรับขนาดเธรดได้สูงสุด
  2. จากนั้นจัดคิวจนกว่าจะเต็ม 1,000 งาน
  3. จากนั้นบล็อกผู้โทรจนกว่าจะมีพื้นที่ว่างสำหรับคิว

นอกจากนี้หากคุณจำเป็นต้องใช้offer(...)ในตอน RejectedExecutionHandlerนั้นคุณสามารถใช้offer(E, long, TimeUnit)วิธีนี้แทนLong.MAX_VALUEเป็นระยะหมดเวลาได้

คำเตือน:

หากคุณคาดหวังว่าจะมีการเพิ่มงานไปยังตัวดำเนินการหลังจากปิดระบบไปแล้วคุณอาจต้องการฉลาดกว่านี้ในการRejectedExecutionExceptionเลิกใช้งานแบบกำหนดเองของเราRejectedExecutionHandlerเมื่อบริการเรียกใช้งานถูกปิดลง ขอบคุณ @RaduToader ที่ชี้ให้เห็นสิ่งนี้

แก้ไข:

การปรับแต่งคำตอบนี้อีกประการหนึ่งอาจเป็นการถาม TPE ว่ามีเธรดที่ไม่ได้ใช้งานหรือไม่และจัดคิวรายการเท่านั้นหากมี คุณจะต้องสร้างคลาสที่แท้จริงสำหรับสิ่งนี้และเพิ่มourQueue.setThreadPoolExecutor(tpe);เมธอดเข้าไป

offer(...)วิธีการของคุณอาจมีลักษณะดังนี้:

  1. ตรวจสอบดูว่าtpe.getPoolSize() == tpe.getMaximumPoolSize()ในกรณีที่โทรsuper.offer(...).
  2. ถ้าอย่างtpe.getPoolSize() > tpe.getActiveCount()นั้นโทรไปsuper.offer(...)เพราะดูเหมือนว่าจะไม่มีเธรด
  3. มิฉะนั้นfalseให้กลับไปแยกด้ายอื่น

อาจเป็นเช่นนี้:

int poolSize = tpe.getPoolSize();
int maximumPoolSize = tpe.getMaximumPoolSize();
if (poolSize >= maximumPoolSize || poolSize > tpe.getActiveCount()) {
    return super.offer(e);
} else {
    return false;
}

โปรดทราบว่าเมธอด get บน TPE มีราคาแพงเนื่องจากเข้าถึงvolatileฟิลด์หรือ (ในกรณีgetActiveCount()) ล็อก TPE และเดินรายการเธรด นอกจากนี้ยังมีเงื่อนไขการแข่งขันที่นี่ที่อาจทำให้งานถูกจัดคิวไม่เหมาะสมหรือเธรดอื่นถูกแยกออกเมื่อมีเธรดที่ไม่ได้ใช้งาน


ฉันยังต่อสู้กับปัญหาเดียวกันจบลงด้วยการแทนที่วิธีการดำเนินการ แต่นี่เป็นทางออกที่ดีจริงๆ :)
Batty

เท่าที่ฉันไม่ชอบความคิดในการทำลายสัญญาQueueเพื่อให้บรรลุเป้าหมายนี้คุณไม่ได้อยู่คนเดียวในความคิดของคุณ: groovy-programming.com/post/26923146865
bstempi

3
คุณไม่รู้สึกแปลกตรงนี้ที่งานสองสามงานแรกจะถูกจัดคิวและหลังจากนั้นเธรดใหม่จะเกิดขึ้นหรือไม่? ตัวอย่างเช่นถ้าหัวข้อหลักของคุณหนึ่งกำลังยุ่งอยู่กับงานที่ยาวทำงานเดียวและคุณโทรexecute(runnable)จากนั้นrunnableจะมีการเพิ่มเพียงเพื่อให้คิว ถ้าคุณโทรexecute(secondRunnable)มาก็secondRunnableจะถูกเพิ่มลงในคิว แต่ตอนนี้ถ้าคุณโทรexecute(thirdRunnable)แล้วthirdRunnableจะถูกเรียกใช้ในเธรดใหม่ runnableและsecondRunnableเรียกใช้เพียงครั้งเดียวthirdRunnable(หรือเดิมงานยาวทำงาน) จะเสร็จสิ้น
Robert Tupelo-Schneck

1
ใช่โรเบิร์ตพูดถูกในสภาพแวดล้อมที่มีหลายเธรดสูงบางครั้งคิวจะเพิ่มขึ้นในขณะที่มีเธรดฟรีให้ใช้ โซลูชันด้านล่างซึ่งขยาย TPE - ทำงานได้ดีขึ้นมาก ฉันคิดว่าคำแนะนำของโรเบิร์ตควรถูกระบุว่าเป็นคำตอบแม้ว่าการแฮ็กข้างต้นจะน่าสนใจก็ตาม
Wanna Know All

1
"RejectedExecutionHandler" ช่วยผู้ดำเนินการในการปิดระบบ ตอนนี้คุณถูกบังคับให้ใช้ shutdownNow () เป็น shutdown () ไม่ได้ป้องกันงานใหม่ที่จะเพิ่ม (เนื่องจากมีการร้องขอ)
Radu Toader

30

ฉันมีคำตอบอีกสองข้อสำหรับคำถามนี้แล้ว แต่ฉันสงสัยว่าคำตอบนี้ดีที่สุด

มันขึ้นอยู่กับเทคนิคของคำตอบที่ยอมรับในปัจจุบันได้แก่ :

  1. แทนที่offer()เมธอดของคิวเพื่อ (บางครั้ง) ส่งคืนเท็จ
  2. ซึ่งทำให้เกิดThreadPoolExecutorเธรดใหม่หรือปฏิเสธงานและ
  3. ตั้งค่าRejectedExecutionHandlerเพื่อจัดคิวงานจริงเมื่อปฏิเสธ

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

การแก้ปัญหาคือการใช้ Java 7 LinkedTransferQueueและมีการโทรoffer() tryTransfer()เมื่อมีเธรดผู้บริโภคที่รออยู่งานก็จะถูกส่งต่อไปยังเธรดนั้น มิฉะนั้นoffer()จะส่งคืนเท็จและThreadPoolExecutorจะเกิดเธรดใหม่

    BlockingQueue<Runnable> queue = new LinkedTransferQueue<Runnable>() {
        @Override
        public boolean offer(Runnable e) {
            return tryTransfer(e);
        }
    };
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue);
    threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    });

ฉันต้องยอมรับมันดูสะอาดที่สุดสำหรับฉัน ข้อเสียเพียงอย่างเดียวของโซลูชันคือ LinkedTransferQueue ไม่ถูกผูกไว้ดังนั้นคุณจึงไม่ได้รับคิวงานที่มีขีด จำกัด ความจุโดยไม่ต้องทำงานพิเศษ
Yeroc

มีปัญหาเมื่อพูลขยายถึงขนาดสูงสุด สมมติว่าพูลปรับขนาดขึ้นเป็นขนาดสูงสุดและทุกเธรดกำลังดำเนินการตามภารกิจเมื่อ runnable ส่งข้อเสนอนี้ im จะส่งคืนเท็จและ ThreadPoolExecutor พยายามเพิ่มเธรดการทำงาน แต่พูลถึงจำนวนสูงสุดแล้วดังนั้น runnable ก็จะถูกปฏิเสธ ตามที่ปฏิเสธ ExceHandler ที่คุณเขียนมันจะถูกเสนอในคิวอีกครั้งซึ่งส่งผลให้การเต้นรำของลิงเกิดขึ้นตั้งแต่ต้นอีกครั้ง
Sudheera

1
@Sudheera ฉันเชื่อว่าคุณเข้าใจผิด queue.offer()เนื่องจากเป็นการโทรจริงLinkedTransferQueue.tryTransfer()จะส่งคืนเท็จและไม่จัดคิวงาน อย่างไรก็ตามการRejectedExecutionHandlerโทรqueue.put()ซึ่งไม่ล้มเหลวและทำให้งานมีคิว
Robert Tupelo-Schneck

1
@ RobertTupelo-Schneck มีประโยชน์และดีมาก!
ยูจีน

1
@ RobertTupelo-Schneck ทำงานอย่างมีเสน่ห์! ฉันไม่รู้ว่าทำไมไม่มีอะไรแบบนั้นนอกกรอบใน java
Georgi

28

allowCoreThreadTimeOut(true)ชุดขนาดหลักและขนาดสูงสุดเป็นค่าเดียวกันและช่วยให้หัวข้อหลักจะถูกลบออกจากสระว่ายน้ำด้วย


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

ขอบคุณ! เป็นเพียงวิธีที่ง่ายที่สุดในการทำเช่นนั้น
Dmitry Ovchinnikov

7

หมายเหตุ: ตอนนี้ผมต้องการและให้คำแนะนำคำตอบอื่น ๆ ของฉัน

นี่คือเวอร์ชันที่ให้ความรู้สึกตรงไปตรงมามากขึ้นสำหรับฉัน: เพิ่ม corePoolSize (สูงสุดถึงขีด จำกัด maximumPoolSize) เมื่อใดก็ตามที่มีการเรียกใช้งานใหม่จากนั้นลด corePoolSize (ลงจนถึงขีด จำกัด ของผู้ใช้ที่ระบุ "ขนาดพูลแกน") เมื่อใดก็ตาม งานเสร็จสมบูรณ์

หากต้องการกล่าวอีกนัยหนึ่งให้ติดตามจำนวนงานที่รันหรือจัดคิวและตรวจสอบให้แน่ใจว่า corePoolSize เท่ากับจำนวนงานตราบเท่าที่อยู่ระหว่างผู้ใช้ที่ระบุ "ขนาดพูลหลัก" และ maximumPoolSize

public class GrowBeforeQueueThreadPoolExecutor extends ThreadPoolExecutor {
    private int userSpecifiedCorePoolSize;
    private int taskCount;

    public GrowBeforeQueueThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        userSpecifiedCorePoolSize = corePoolSize;
    }

    @Override
    public void execute(Runnable runnable) {
        synchronized (this) {
            taskCount++;
            setCorePoolSizeToTaskCountWithinBounds();
        }
        super.execute(runnable);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable throwable) {
        super.afterExecute(runnable, throwable);
        synchronized (this) {
            taskCount--;
            setCorePoolSizeToTaskCountWithinBounds();
        }
    }

    private void setCorePoolSizeToTaskCountWithinBounds() {
        int threads = taskCount;
        if (threads < userSpecifiedCorePoolSize) threads = userSpecifiedCorePoolSize;
        if (threads > getMaximumPoolSize()) threads = getMaximumPoolSize();
        setCorePoolSize(threads);
    }
}

ตามที่เขียนไว้ในชั้นเรียนไม่สนับสนุนการเปลี่ยนผู้ใช้ที่ระบุ corePoolSize หรือ maximumPoolSize หลังจากการก่อสร้างและไม่สนับสนุนการจัดการคิวงานโดยตรงหรือผ่านทางหรือremove()purge()


ฉันชอบมันยกเว้นsynchronizedบล็อก คุณสามารถโทรไปที่คิวเพื่อรับจำนวนงานได้ไหม หรืออาจจะใช้AtomicInteger?
สีเทา

ฉันต้องการหลีกเลี่ยงพวกเขา แต่ปัญหาคือสิ่งนี้ หากมีจำนวนของexecute()สายในหัวข้อแยกกันเป็นไปได้ (1) คิดออกวิธีการหลายหัวข้อที่มีความจำเป็น (2) setCorePoolSizeไปยังหมายเลขที่และ (3) super.execute()การโทร หากขั้นตอนที่ (1) และ (2) ไม่ตรงกันฉันไม่แน่ใจว่าจะป้องกันการจัดลำดับที่ไม่ดีได้อย่างไรโดยที่คุณตั้งค่าขนาดแกนพูลเป็นตัวเลขที่ต่ำกว่าหลังจากที่มีตัวเลขสูงกว่า ด้วยการเข้าถึงโดยตรงไปยังฟิลด์ superclass สิ่งนี้สามารถทำได้โดยใช้การเปรียบเทียบและตั้งค่าแทน แต่ฉันไม่เห็นวิธีที่ชัดเจนในการทำในคลาสย่อยโดยไม่ต้องซิงโครไนซ์
Robert Tupelo-Schneck

ฉันคิดว่าบทลงโทษสำหรับสภาพการแข่งขันนั้นค่อนข้างต่ำตราบเท่าที่taskCountสนามนั้นถูกต้อง (เช่นกAtomicInteger) หากเธรดสองเธรดคำนวณขนาดพูลซ้ำกันทันทีควรมีค่าที่เหมาะสม ถ้าอันที่ 2 หดเธรดคอร์ก็ต้องเห็นคิวหรืออะไรบางอย่างลดลง
สีเทา

1
น่าเศร้าที่ฉันคิดว่ามันแย่กว่านั้น สมมติว่างาน 10 และ 11 โทรexecute(). แต่ละคนจะโทรatomicTaskCount.incrementAndGet()และพวกเขาจะได้รับ 10 และ 11 ตามลำดับ แต่หากไม่มีการซิงโครไนซ์ (จากการรับจำนวนงานและการตั้งค่าขนาดพูลคอร์) คุณจะได้รับ (1) งาน 11 ตั้งค่าขนาดพูลแกนเป็น 11, (2) งาน 10 ตั้งค่าขนาดพูลคอร์เป็น 10, (3) งาน 10 สายsuper.execute()(4) งาน 11 สายsuper.execute()และมีการจัดคิว
Robert Tupelo-Schneck

2
ฉันให้วิธีนี้กับการทดสอบอย่างจริงจังและเห็นได้ชัดว่าดีที่สุด ในสภาพแวดล้อมแบบมัลติเธรดสูงบางครั้งจะยังคงจัดลำดับเมื่อมีเธรดฟรี (เนื่องจากลักษณะการดำเนินการ TPE ฟรีเธรด) แต่เกิดขึ้นไม่บ่อยนักเมื่อเทียบกับโซลูชันที่ทำเครื่องหมายเป็นคำตอบซึ่งเงื่อนไขการแข่งขันมีโอกาสมากขึ้นในการ เกิดขึ้นดังนั้นสิ่งนี้จึงเกิดขึ้นค่อนข้างมากในการทำงานแบบมัลติเธรดแต่ละครั้ง
Wanna Know All

6

เรามี subclass ของThreadPoolExecutorที่ต้องใช้เพิ่มเติมและแทนที่creationThresholdexecute

public void execute(Runnable command) {
    super.execute(command);
    final int poolSize = getPoolSize();
    if (poolSize < getMaximumPoolSize()) {
        if (getQueue().size() > creationThreshold) {
            synchronized (this) {
                setCorePoolSize(poolSize + 1);
                setCorePoolSize(poolSize);
            }
        }
    }
}

อาจจะช่วยได้เช่นกัน แต่แน่นอนว่าคุณดูมีศิลปะมากกว่า ...


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

ตอนนี้ฉันคิดเกี่ยวกับมันมากกว่านี้วิธีแก้ปัญหานี้ดีกว่าของฉันในแง่ของการตรวจสอบขนาดของคิว ฉันได้ปรับเปลี่ยนคำตอบของฉันเพื่อให้offer(...)วิธีนี้คืนค่าตามfalseเงื่อนไขเท่านั้น ขอบคุณ!
สีเทา

4

คำตอบที่แนะนำสามารถแก้ไขปัญหาได้เพียงหนึ่ง (1) ปัญหากับพูลเธรด JDK:

  1. พูลเธรด JDK เอนเอียงไปทางการจัดคิว ดังนั้นแทนที่จะสร้างเธรดใหม่พวกเขาจะจัดคิวงาน เฉพาะในกรณีที่คิวถึงขีด จำกัด เธรดพูลจะเกิดเธรดใหม่

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

ไม่พอใจกับพฤติกรรมข้างต้นฉันจึงดำเนินการสระว่ายน้ำเพื่อเอาชนะข้อบกพร่องข้างต้น

วิธีแก้ไข 2) การใช้การตั้งเวลา Lifo ช่วยแก้ปัญหาได้ ความคิดนี้นำเสนอโดย Ben Maurer ในการประชุม ACM applicative 2015: Systems @ Facebook scale

ดังนั้นการติดตั้งใหม่จึงถือกำเนิดขึ้น:

LifoThreadPoolExecutorSQP

เพื่อให้ห่างไกลการดำเนินงานนี้จะช่วยปรับปรุงการดำเนินการ perfomance async สำหรับเซล

การใช้งานสามารถหมุนได้เพื่อลดค่าใช้จ่ายของสวิตช์บริบทซึ่งให้ประสิทธิภาพที่เหนือกว่าสำหรับกรณีใช้งานบางกรณี

หวังว่ามันจะช่วย ...

PS: JDK Fork Join Pool ใช้ ExecutorService และทำงานเป็นเธรดพูล "ปกติ" การใช้งานเป็นไปอย่างมีประสิทธิภาพโดยใช้การตั้งเวลา LIFO Thread อย่างไรก็ตามไม่มีการควบคุมขนาดคิวภายในการหมดเวลาการเกษียณอายุ ... และที่สำคัญที่สุดคืองานไม่สามารถทำได้ ถูกขัดจังหวะเมื่อยกเลิก


1
น่าเสียดายที่การใช้งานนี้มีการอ้างอิงภายนอกมากเกินไป ทำให้ไม่มีประโยชน์สำหรับฉัน: - /
Martin L.

1
เป็นจุดที่ดีจริงๆ (ที่ 2) น่าเสียดายที่การใช้งานไม่ชัดเจนจากการอ้างอิงภายนอก แต่ยังสามารถนำไปใช้ได้หากคุณต้องการ
Alexey Vlasov

1

หมายเหตุ: ตอนนี้ผมต้องการและให้คำแนะนำคำตอบอื่น ๆ ของฉัน

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

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

final Runnable SENTINEL_NO_OP = new Runnable() { public void run() { } };

final AtomicInteger waitingThreads = new AtomicInteger(0);

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>() {
    @Override
    public boolean offer(Runnable e) {
        // offer returning false will cause the executor to spawn a new thread
        if (e == SENTINEL_NO_OP) return size() <= waitingThreads.get();
        else return super.offer(e);
    }

    @Override
    public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.poll(timeout, unit);
        } finally {
            waitingThreads.decrementAndGet();
        }
    }

    @Override
    public Runnable take() throws InterruptedException {
        try {
            waitingThreads.incrementAndGet();
            return super.take();
        } finally {
            waitingThreads.decrementAndGet();
        }
    }
};

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 50, 60, TimeUnit.SECONDS, queue) {
    @Override
    public void execute(Runnable command) {
        super.execute(command);
        if (getQueue().size() > waitingThreads.get()) super.execute(SENTINEL_NO_OP);
    }
};
threadPool.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r == SENTINEL_NO_OP) return;
        else throw new RejectedExecutionException();            
    }
});

0

ทางออกที่ดีที่สุดที่ฉันคิดได้คือการต่อยอด

ThreadPoolExecutorเสนอวิธีการเกี่ยวกับเบ็ดสองสามวิธี: beforeExecuteและafterExecute. ในส่วนขยายของคุณคุณสามารถใช้คิวที่มีขอบเขตเพื่อป้อนงานและคิวที่ไม่ถูกผูกไว้ที่สองเพื่อจัดการกับการล้น เมื่อมีคนโทรsubmitมาคุณสามารถลองส่งคำขอลงในคิวที่มีขอบเขต หากคุณพบข้อยกเว้นคุณเพียงแค่ติดงานในคิวล้นของคุณ จากนั้นคุณสามารถใช้afterExecuteเบ็ดเพื่อดูว่ามีอะไรอยู่ในคิวล้นหรือไม่หลังจากเสร็จสิ้นภารกิจ ด้วยวิธีนี้ผู้ดำเนินการจะดูแลสิ่งต่างๆในคิวที่มีขอบเขตก่อนและดึงออกจากคิวที่ไม่มีการผูกมัดนี้โดยอัตโนมัติเมื่อเวลาอนุญาต

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


ฉันไม่ชอบวิธีแก้ปัญหานี้ ฉันค่อนข้างแน่ใจว่า ThreadPoolExecutor ไม่ได้ออกแบบมาเพื่อการสืบทอด
scottb

จริงๆแล้วมีตัวอย่างของส่วนขยายใน JavaDoc พวกเขาระบุว่าส่วนใหญ่อาจใช้วิธีการเชื่อมต่อ แต่จะบอกคุณว่ามีอะไรอีกบ้างที่คุณต้องระวังเมื่อขยาย
bstempi

0

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

ฉันต้องการงานที่จะดำเนินการจากเธรดที่สร้างโดยพูลและมีคิว ubounded สำหรับการตั้งเวลาในขณะที่จำนวนเธรดภายในพูลอาจเพิ่มขึ้นหรือลดขนาดระหว่างcorePoolSizeและmaximumPoolSizeดังนั้น ...

ฉันลงเอยด้วยการวางสำเนาแบบเต็มจากThreadPoolExecutorและเปลี่ยนวิธีการดำเนินการเล็กน้อยเพราะ น่าเสียดายที่ไม่สามารถทำได้โดยการขยาย (เรียกว่า private method)

ฉันไม่ต้องการสร้างเธรดใหม่ทันทีเมื่อมีคำขอใหม่มาถึงและเธรดทั้งหมดไม่ว่าง (เนื่องจากฉันมีงานที่มีอายุสั้นโดยทั่วไป) ฉันได้เพิ่มเกณฑ์แล้ว แต่อย่าลังเลที่จะเปลี่ยนแปลงตามความต้องการของคุณ (สำหรับ IO ส่วนใหญ่จะเป็นการดีกว่าที่จะลบเกณฑ์นี้)

private final AtomicInteger activeWorkers = new AtomicInteger(0);
private volatile double threshold = 0.7d;

protected void beforeExecute(Thread t, Runnable r) {
    activeWorkers.incrementAndGet();
}
protected void afterExecute(Runnable r, Throwable t) {
    activeWorkers.decrementAndGet();
}
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) {
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {
                this.addWorker((Runnable) null, false);
            }
            //>>change start
            else if (workerCountOf(recheck) < maximumPoolSize //
                && (activeWorkers.get() > workerCountOf(recheck) * threshold
                    || workQueue.size() > workerCountOf(recheck) * threshold)) {
                this.addWorker((Runnable) null, false);
            }
            //<<change end
        } else if (!this.addWorker(command, false)) {
            this.reject(command);
        }
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.