Як зробити блок методу submit () ThreadPoolExecutor, якщо він насичений?


102

Я хочу створити ThreadPoolExecutorтаке, що коли він досяг максимального розміру і черга заповнена, submit()метод блокується при спробі додати нові завдання. Чи потрібно мені реалізувати RejectedExecutionHandlerдля цього користувацький звичай чи існує існуючий спосіб зробити це за допомогою стандартної бібліотеки Java?


2
Це те, що ви хочете, що-небудь, як метод блокування черги Array (() блоку?)
екстранеон


2
@bacar Я не згоден. Це питання виглядає більш цінним (крім того, що є старшим).
JasonMArcher

Відповіді:


47

Одне з можливих рішень, які я щойно знайшов:

public class BoundedExecutor {
    private final Executor exec;
    private final Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }

    public void submitTask(final Runnable command)
            throws InterruptedException, RejectedExecutionException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
            throw e;
        }
    }
}

Чи є інші рішення? Я вважаю за краще щось на основі, RejectedExecutionHandlerоскільки це здається стандартним способом вирішення подібних ситуацій.


2
Чи існує тут умова перегонів між моментом, коли семафор звільняється в остаточному підсумку, і семафор набувається?
volni

2
Як було сказано вище, ця реалізація є хибною, оскільки семафор звільняється перед виконанням завдання. Було б краще використовувати метод java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable)
FelixM

2
@FelixM: використання java.util.concurrent.ThreadPoolExecutor # afterExecute (Runnable, Throwable) не вирішить проблему, оскільки afterExecute викликається відразу після task.run () у java.util.concurrent.ThreadPoolExecutor # runWorker (Worker w), взяття наступного елемента з черги (дивлячись на вихідний код openjdk 1.7.0.6).
Яан

1
Ця відповідь - із книги "
Бракує

11
Ця відповідь не зовсім правильна, також це зауваження. Цей фрагмент коду дійсно походить від Java Concurrency in Practice, і це правильно, якщо врахувати його контекст . У книзі чітко висловлюється буквально: "При такому підході використовуйте необмежену чергу (...) і встановіть обмежений на семафорі рівний розміру пулу плюс кількості завдань, що стоять у черзі, які ви хочете дозволити". З необмеженою чергою завдання ніколи не будуть відхилені, тому повторне скидання винятку абсолютно марно! Що, на мій погляд, також є причиною того, чому throw e;це НЕ в книзі. JCIP правильний!
Тиммос

30

Ви можете використовувати ThreadPoolExecutor та блокуючу чергу:

public class ImageManager {
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(blockQueueSize);
    RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    private ExecutorService executorService =  new ThreadPoolExecutor(numOfThread, numOfThread, 
        0L, TimeUnit.MILLISECONDS, blockingQueue, rejectedExecutionHandler);

    private int downloadThumbnail(String fileListPath){
        executorService.submit(new yourRunnable());
    }
}

Я просто хотів би сказати, що це було шалено швидким і простим рішенням для реалізації, яке спрацювало дуже добре!
Іван

58
Це виконує відхилені завдання на потоці подання. Що функціонально не відповідає вимогам ОП.
Сприйняття

4
Це виконує завдання "в виклику", а не блокувати, щоб поставити його в чергу, що може мати негативні наслідки, як, наприклад, якщо кілька потоків називають це таким чином, більше, ніж "розмір черги" буде виконуватися, і якщо якщо це завдання займе більше часу, ніж очікувалося, ваша "виробнича" нитка може не тримати виконавця зайнятим. Але тут чудово працювали!
rogerdpack

4
Зрозуміло: це не блокується, коли TPE насичується. Це просто альтернатива, а не рішення.
Тиммос

1
Оновлено: це в значній мірі відповідає «дизайну TPE» і «природно блокує» клієнтські потоки, надаючи їм тести переповнення. Це має охоплювати більшість випадків використання, але не всі вони, звичайно, і ви повинні розуміти, що відбувається під капотом.
Майк

12

Вам слід скористатись тим CallerRunsPolicy, що виконує відхилене завдання у виклику. Таким чином, він не може подати виконавцю будь-які нові завдання до тих пір, поки це завдання не буде виконано, після чого з’являться кілька вільних ниток пулу або процес повториться.

http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/ThreadPoolExecutor.CallerRunsPolicy.html

З документів:

Відхилені завдання

Нові завдання, представлені у методі Execute (java.lang.Runnable), будуть відхилені, коли Виконавець вимкнений, а також коли Виконавець використовує обмежені межі як для максимальних потоків, так і для ємності робочої черги, і є насиченим. У будь-якому випадку метод Execute викликає метод RejectedExecutionHandler.rejectedExecution (java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) методу його RejectedExecutionHandler. Надано чотири заздалегідь визначені політики обробника:

  1. У програмі ThreadPoolExecutor.AbortPolicy за замовчуванням обробник кидає програму RejectedExecutionException за умови відхилення.
  2. У ThreadPoolExecutor.CallerRunsPolicy нитка, яка викликає виконання, сама виконує завдання. Це забезпечує простий механізм контролю зворотного зв'язку, який уповільнить швидкість подання нових завдань.
  3. У ThreadPoolExecutor.DiscardPolicy завдання, яке неможливо виконати, просто відкидається.
  4. У ThreadPoolExecutor.DiscardOldestPolicy, якщо виконавець не вимкнений, завдання на чолі робочої черги опускається, а потім виконання повторюється (що може знову не вдатися, внаслідок чого це повториться.)

Також обов'язково використовуйте обмежену чергу, наприклад ArrayBlockingQueue, при виклику ThreadPoolExecutor конструктора. Інакше нічого не буде відхилено.

Редагувати: у відповідь на ваш коментар встановіть розмір черги ArrayBlockingQue, що дорівнює максимальному розміру пулу потоків, і використовуйте AbortPolicy.

Редагування 2: Гаразд, я бачу, до чого ти потрапляєш Що з цього приводу: замініть beforeExecute()метод, щоб перевірити, getActiveCount()чи не перевищує його getMaximumPoolSize(), і якщо він є, сплять і спробуйте ще раз?


3
Я хочу, щоб кількість одночасно виконаних завдань було жорстко обмежене (за кількістю потоків у Виконавці), тому я не можу дозволити потокам абонента виконувати ці завдання самостійно.
Fixpoint

1
AbortPolicy призведе до того, що потік абонента отримає RejectedExecutionException, тоді як мені потрібно лише блокувати.
Fixpoint

2
Я прошу заблокувати, а не сон і опитування;)
Fixpoint

@danben: Ви не маєте на увазі CallerRunsPolicy ?
користувач359996

7
Проблема з CallerRunPolicy полягає в тому, що якщо у вас є один виробник потоків, у вас часто з’являться теми, які не використовуються, якщо трапляється тривале завдання, щоб його відхилити (адже інші завдання в пулі потоків будуть закінчені, поки тривала задача триває біг).
Адам Гент

6

У сплячому режимі BlockPolicyце просто і може робити те, що ви хочете:

Дивіться: Виконавці.java

/**
 * A handler for rejected tasks that will have the caller block until
 * space is available.
 */
public static class BlockPolicy implements RejectedExecutionHandler {

    /**
     * Creates a <tt>BlockPolicy</tt>.
     */
    public BlockPolicy() { }

    /**
     * Puts the Runnable to the blocking queue, effectively blocking
     * the delegating thread until space is available.
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        try {
            e.getQueue().put( r );
        }
        catch (InterruptedException e1) {
            log.error( "Work discarded, thread was interrupted while waiting for space to schedule: {}", r );
        }
    }
}

4
По-друге, це дуже погана ідея. Я не рекомендую вам його використовувати. З поважних причин дивіться тут: stackoverflow.com/questions/3446011/…
Nate Murray

Крім того, для цього не використовується "стандартна бібліотека Java", відповідно до запиту ОП. Видалити?
користувач359996

1
Вау, це так потворно. В основному це рішення заважає внутрішнім системам TPE. Явадок ThreadPoolExecutorнавіть говорить буквально: "Метод getQueue () дозволяє отримати доступ до черги робіт для моніторингу та налагодження. Використання цього методу для будь-яких інших цілей сильно не рекомендується.". Те, що це є в бібліотеці, яка настільки відома, абсолютно сумно бачити.
Тиммос

1
com.amazonaws.services.simpleworkflow.flow.worker.BlockCallerPolicy подібний.
Адріан Бейкер

6

BoundedExecutorВідповідь Наведений вище від Java Паралелізм на практиці працює тільки якщо ви використовуєте необмежену чергу для Виконавця, або семафор пов'язаний не більше , ніж розмір черги. Семафор поділяється між потоком, що подає, і потоками в пулі, що дозволяє наситити виконавця, навіть якщо розмір черги <пов'язаний <= (розмір черги + розмір пулу).

Використання CallerRunsPolicyсправедливо лише в тому випадку, якщо ваші завдання не виконуються назавжди, і в цьому випадку потік, який надсилається, залишиться rejectedExecutionназавжди, і погана ідея, якщо ваші завдання потребують тривалого часу, оскільки нитка, яка надсилає, не може подати нові завдання або робити що-небудь інше, якщо він сам виконує завдання.

Якщо це не прийнятно, я пропоную перевірити розмір обмеженої черги виконавця перед тим, як подати завдання. Якщо черга заповнена, зачекайте короткий час, перш ніж повторити спробу. Пропускна здатність постраждає, але я пропоную це більш просте рішення, ніж багато інших запропонованих рішень, і ви гарантовано жодні завдання не буде відхилено.


Я не впевнений, як перевірити довжину черги перед подачею гарантій без відхилених завдань у багатопотоковому середовищі з кількома виробниками завдань. Це не звучить безпечно для потоків.
Тім

5

Я знаю, це хакер, але, на мій погляд, найчистіший злом між запропонованими тут ;-)

Оскільки ThreadPoolExecutor використовує блоку чергу "пропозиція" замість "поставити", дозволяє переосмислити поведінку "пропозиції" черзі блокування:

class BlockingQueueHack<T> extends ArrayBlockingQueue<T> {

    BlockingQueueHack(int size) {
        super(size);
    }

    public boolean offer(T task) {
        try {
            this.put(task);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    }
}

ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 2, 1, TimeUnit.MINUTES, new BlockingQueueHack(5));

Я перевірив це і, здається, працює. Реалізація певної політики тайм-ауту залишається вправою для читання.


Дивіться stackoverflow.com/a/4522411/2601671 для очищеної версії цього. Я згоден, це найчистіший спосіб зробити це.
Трентон

3

Наступний клас обертається навколо ThreadPoolExecutor і використовує Semaphore для блокування, тоді черга роботи заповнена:

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

Цей клас обгортки базується на рішенні, поданому в книзі Брайана Геца в книзі Java Concurrency in Practice. Рішення в книзі приймає лише два конструкторські параметри: a Executorі обмежений, що використовується для семафору. Це показано у відповіді, наданій Fixpoint. У цьому підході є проблема: він може потрапити в стан, коли нитки пулу зайняті, черга повна, але семафор щойно видав дозвіл. (semaphore.release() в остаточному блоці). У такому стані нове завдання може захопити щойно випущений дозвіл, але його відхилено, оскільки черга завдань заповнена. Звичайно, це не те, чого ти хочеш; ви хочете заблокувати в цьому випадку.

Щоб вирішити це, ми повинні використовувати необмежену чергу, як чітко згадує JCiP. Семафор діє як охорона, надаючи ефект розміру віртуальної черги. Це побічно впливає на те, що цілком можливо, що підрозділ може містити maxPoolSize + virtualQueueSize + maxPoolSizeзавдання. Чому так? Через те, semaphore.release()що, нарешті, блок. Якщо всі потоки пулу викликають цей вислів одночасно, тоді maxPoolSizeдозволу звільняються, дозволяючи однаковій кількості завдань входити в блок. Якби ми використовували обмежену чергу, вона все одно буде повною, що призведе до відхиленого завдання. Тепер, оскільки ми знаємо, що це відбувається лише тоді, коли нитка пулу майже виконана, це не проблема. Ми знаємо, що нитка пулу не блокується, тому незабаром буде взято завдання з черги.

Однак ви можете використовувати обмежену чергу. Просто переконайтесь, що його розмір дорівнює virtualQueueSize + maxPoolSize. Більший розмір марний, семафор не дозволить пускати більше предметів. Менші розміри призводять до відхилених завдань. Шанс відхилення завдань збільшується зі зменшенням розміру. Наприклад, скажіть, що ви хочете обмеженого виконавця з maxPoolSize = 2 та virtualQueueSize = 5. Потім візьміть семафор з 5 + 2 = 7 дозволів і фактичним розміром черги 5 + 2 = 7. Реальна кількість завдань, які можуть бути в підрозділі, становить 2 + 5 + 2 = 9. Коли виконавець заповнений (5 завдань у черзі, 2 у пулі потоків, тому доступно 0 дозволів) та ВСІ нитки пулу випускають свої дозволи, то рівно 2 дозволу можуть бути отримані завданнями, що надходять.

Тепер рішення від JCiP є дещо громіздким у використанні, оскільки воно не виконує всіх цих обмежень (необмеженої черги або обмежених цими математичними обмеженнями тощо). Я думаю, що це слугує лише гарним прикладом для демонстрації того, як ви можете створювати нові безпечні класи для потоків на основі наявних частин, але не як повноцінного класу для багаторазового використання. Я не думаю, що останній був задумом автора.


2

ви можете використовувати спеціальний RejectedExecutionHandler, як це

ThreadPoolExecutor tp= new ThreadPoolExecutor(core_size, // core size
                max_handlers, // max size 
                timeout_in_seconds, // idle timeout 
                TimeUnit.SECONDS, queue, new RejectedExecutionHandler() {
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        // This will block if the queue is full
                        try {
                            executor.getQueue().put(r);
                        } catch (InterruptedException e) {
                            System.err.println(e.getMessage());
                        }

                    }
                });

1
Документи для getQueue () прямо зазначають, що доступ до черги завдань призначений насамперед для налагодження та моніторингу.
Чаді

0

Створіть власну чергу блокування, яку повинен використовувати Виконавець, з поведінкою блокування, яку ви шукаєте, при цьому завжди повертайте наявну залишкову ємність (гарантуючи, що виконавець не намагатиметься створити більше потоків, ніж його основний пул, або запустить обробник відхилення).

Я вірю, що це допоможе вам блокувати поведінку, яку ви шукаєте. Обробник відхилення ніколи не підходить до рахунку, оскільки це вказує, що виконавець не може виконати завдання. Я можу передбачити, що ви отримуєте певну форму "зайнятого очікування" в обробнику. Це не те, що ви хочете, ви хочете чергу за виконавцем, який блокує абонента ...


2
ThreadPoolExecutorвикористовує offerметод для додавання завдань у чергу. Якби я створив звичай, BlockingQueueякий блокується offer, це порушило б BlockingQueueконтракт.
Fixpoint

@Shooshpanchick, це порушить контракт на BlockingQueues. і що? якщо ви настільки зацікавлені, ви можете явно ввімкнути поведінку лише під час подання () (це займе ThreadLocal)
bestsss

Дивіться також цю відповідь на інше запитання, яке описує цю альтернативу.
Роберт Тупело-Шнек

Чи є причина, чому ThreadPoolExecutorбуло реалізовано використовувати, offerа не put(блокуюча версія)? Крім того, якби спосіб клієнтського коду міг сказати, який саме використовувати, коли багато людей, які намагаються вручну скористатися спеціальними рішеннями, було б полегшене
асгс

0

Щоб уникнути проблем із рішенням @FixPoint Можна використати ListeningExecutorService і випустити семафор onSuccess і onFailure всередині FutureCallback.


Це має ті ж самі притаманні проблеми, що й просто обгортання, Runnableоскільки ці методи все ще закликаються перед очищенням працівників у звичайному режимі ThreadPoolExecutor. Тобто вам все одно доведеться обробляти винятки щодо відхилення.
Адам Гент

0

Нещодавно я виявив це питання з тією ж проблемою. ОП не говорить про це прямо, але ми не хочемо використовувати цеRejectedExecutionHandler що виконує завдання на потоці відправника, оскільки це буде недостатньо використовувати робочі потоки, якщо це завдання триває.

Читаючи всі відповіді та коментарі, зокрема хибне рішення семафору або використовуючи, afterExecuteя детальніше ознайомився з кодом ThreadPoolExecutor, щоб побачити, чи є вихід. Я був здивований, побачивши, що існує понад 2000 рядків (коментованого) коду, деякі з яких викликають запаморочення . Враховуючи досить просту вимогу, яку я насправді маю --- один виробник, кілька споживачів, нехай виробник блокує, коли жоден споживач не може взяти на роботу --- я вирішив прийняти власне рішення. Це не ExecutorServiceпросто, а просто Executor. І вона не пристосовує кількість ниток до робочого навантаження, а вміщує лише фіксовану кількість ниток, що також відповідає моїм вимогам. Ось код. Не соромтеся зважати на це :-)

package x;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;

/**
 * distributes {@code Runnable}s to a fixed number of threads. To keep the
 * code lean, this is not an {@code ExecutorService}. In particular there is
 * only very simple support to shut this executor down.
 */
public class ParallelExecutor implements Executor {
  // other bounded queues work as well and are useful to buffer peak loads
  private final BlockingQueue<Runnable> workQueue =
      new SynchronousQueue<Runnable>();
  private final Thread[] threads;

  /*+**********************************************************************/
  /**
   * creates the requested number of threads and starts them to wait for
   * incoming work
   */
  public ParallelExecutor(int numThreads) {
    this.threads = new Thread[numThreads];
    for(int i=0; i<numThreads; i++) {
      // could reuse the same Runner all over, but keep it simple
      Thread t = new Thread(new Runner());
      this.threads[i] = t;
      t.start();
    }
  }
  /*+**********************************************************************/
  /**
   * returns immediately without waiting for the task to be finished, but may
   * block if all worker threads are busy.
   * 
   * @throws RejectedExecutionException if we got interrupted while waiting
   *         for a free worker
   */
  @Override
  public void execute(Runnable task)  {
    try {
      workQueue.put(task);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
      throw new RejectedExecutionException("interrupt while waiting for a free "
          + "worker.", e);
    }
  }
  /*+**********************************************************************/
  /**
   * Interrupts all workers and joins them. Tasks susceptible to an interrupt
   * will preempt their work. Blocks until the last thread surrendered.
   */
  public void interruptAndJoinAll() throws InterruptedException {
    for(Thread t : threads) {
      t.interrupt();
    }
    for(Thread t : threads) {
      t.join();
    }
  }
  /*+**********************************************************************/
  private final class Runner implements Runnable {
    @Override
    public void run() {
      while (!Thread.currentThread().isInterrupted()) {
        Runnable task;
        try {
          task = workQueue.take();
        } catch (InterruptedException e) {
          // canonical handling despite exiting right away
          Thread.currentThread().interrupt(); 
          return;
        }
        try {
          task.run();
        } catch (RuntimeException e) {
          // production code to use a logging framework
          e.printStackTrace();
        }
      }
    }
  }
}

0

Я вважаю, що існує досить елегантний спосіб вирішити цю проблему за допомогою java.util.concurrent.Semaphoreта делегування поведінки Executor.newFixedThreadPool. Нова служба виконавця виконує нове завдання лише тоді, коли для цього є потік. Блокування управляється Semaphore з кількістю дозволів, рівним кількості потоків. Коли завдання закінчено, він повертає дозвіл.

public class FixedThreadBlockingExecutorService extends AbstractExecutorService {

private final ExecutorService executor;
private final Semaphore blockExecution;

public FixedThreadBlockingExecutorService(int nTreads) {
    this.executor = Executors.newFixedThreadPool(nTreads);
    blockExecution = new Semaphore(nTreads);
}

@Override
public void shutdown() {
    executor.shutdown();
}

@Override
public List<Runnable> shutdownNow() {
    return executor.shutdownNow();
}

@Override
public boolean isShutdown() {
    return executor.isShutdown();
}

@Override
public boolean isTerminated() {
    return executor.isTerminated();
}

@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
    return executor.awaitTermination(timeout, unit);
}

@Override
public void execute(Runnable command) {
    blockExecution.acquireUninterruptibly();
    executor.execute(() -> {
        try {
            command.run();
        } finally {
            blockExecution.release();
        }
    });
}

Я реалізував BoundedExecutor, описаний у програмі Java Concurrency в практиці, і зрозумів, що семафор повинен бути ініціалізований прапором справедливості, встановленим на істинне, щоб забезпечити надання дозволів Semaphore у порядку подання запитів. Зверніться до docs.oracle.com/javase/7/docs/api/java/util/concurrent/… . детальніше
Prahalad Deshpande

0

У мене була така ж потреба в минулому: свого роду блокування черги з фіксованим розміром для кожного клієнта, підкріпленого спільним пулом потоків. Я закінчив писати власний вид ThreadPoolExecutor:

UserThreadPoolExecutor (блокування черги (на клієнта) + нитка путівки (спільне для всіх клієнтів))

Дивіться: https://github.com/d4rxh4wx/UserThreadPoolExecutor

Кожному UserThreadPoolExecutor надається максимальна кількість потоків із спільного ThreadPoolExecutor

Кожен UserThreadPoolExecutor може:

  • подайте завдання виконавцю спільного потоку потоків, якщо його квота не буде досягнута. Якщо досягається його квота, завдання ставиться в чергу (неспоживне блокування, яке чекає процесора). Після того, як одне з поданих завдань буде виконано, квота зменшується, що дозволяє ще одне завдання, яке чекає, буде представлено ThreadPoolExecutor
  • дочекайтеся завершення решти завдань

0

Я знайшов цю політику відхилення в еластичному пошуку клієнта. Він блокує нитку абонента в черзі блокування. Код нижче-

 static class ForceQueuePolicy implements XRejectedExecutionHandler 
 {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 
        {
            try 
            {
                executor.getQueue().put(r);
            } 
            catch (InterruptedException e) 
            {
                //should never happen since we never wait
                throw new EsRejectedExecutionException(e);
            }
        }

        @Override
        public long rejected() 
        {
            return 0;
        }
}

0

Нещодавно у мене виникла потреба домогтися чогось подібного, але на меті ScheduledExecutorService .

Я також повинен був переконатися, що я обробляю затримку, що передається методу, і гарантувати, що або завдання подається на виконання в той час, як абонент очікує, або просто не вдається, таким чином, кидаючи RejectedExecutionException .

Інші методи ScheduledThreadPoolExecutorдля виконання або подання завдання на внутрішній виклик, #scheduleякий все ще в свою чергу викликає переосмислені методи.

import java.util.concurrent.*;

public class BlockingScheduler extends ScheduledThreadPoolExecutor {
    private final Semaphore maxQueueSize;

    public BlockingScheduler(int corePoolSize,
                             ThreadFactory threadFactory,
                             int maxQueueSize) {
        super(corePoolSize, threadFactory, new AbortPolicy());
        this.maxQueueSize = new Semaphore(maxQueueSize);
    }

    @Override
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay,
                                       TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(delay));
        return super.schedule(command, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay,
                                           TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(callable, unit.toMillis(delay));
        return super.schedule(callable, newDelayInMs, TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleAtFixedRate(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long period,
                                                     TimeUnit unit) {
        final long newDelayInMs = beforeSchedule(command, unit.toMillis(initialDelay));
        return super.scheduleWithFixedDelay(command, newDelayInMs, unit.toMillis(period), TimeUnit.MILLISECONDS);
    }

    @Override
    protected void afterExecute(Runnable runnable, Throwable t) {
        super.afterExecute(runnable, t);
        try {
            if (t == null && runnable instanceof Future<?>) {
                try {
                    ((Future<?>) runnable).get();
                } catch (CancellationException | ExecutionException e) {
                    t = e;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null) {
                System.err.println(t);
            }
        } finally {
            releaseQueueUsage();
        }
    }

    private long beforeSchedule(Runnable runnable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(runnable, this);
            return 0;
        }
    }

    private long beforeSchedule(Callable callable, long delay) {
        try {
            return getQueuePermitAndModifiedDelay(delay);
        } catch (InterruptedException e) {
            getRejectedExecutionHandler().rejectedExecution(new FutureTask(callable), this);
            return 0;
        }
    }

    private long getQueuePermitAndModifiedDelay(long delay) throws InterruptedException {
        final long beforeAcquireTimeStamp = System.currentTimeMillis();
        maxQueueSize.tryAcquire(delay, TimeUnit.MILLISECONDS);
        final long afterAcquireTimeStamp = System.currentTimeMillis();
        return afterAcquireTimeStamp - beforeAcquireTimeStamp;
    }

    private void releaseQueueUsage() {
        maxQueueSize.release();
    }
}

Я маю тут код, буду цінувати будь-які відгуки. https://github.com/AmitabhAwasthi/BlockingScheduler


Ця відповідь повністю спирається на зміст зовнішніх посилань. Якщо вони коли-небудь стають недійсними, ваша відповідь була б марною. Тому, будь ласка, відредагуйте свою відповідь та додайте хоча б короткий виклад того, що там можна знайти. Дякую!
Фабіо каже: Відновити Моніку

@fabio: дякую за вказівку. Я додав код туди, так що тепер він має більше сенсу для читачів. Вдячний за ваш коментар :)
Dev Amitabh


0

Мені не завжди подобається CallerRunsPolicy, тим більше, що це дозволяє відхиленому завданню "пропустити чергу" і виконувати перед завданнями, які були подані раніше. Крім того, виконання завдання в потоці виклику може зайняти набагато більше часу, ніж очікування, коли перший слот стане доступним.

Я вирішив цю проблему за допомогою користувацького RejectedExecutionHandler, який просто блокує викликовий потік на деякий час, а потім знову намагається подати завдання:

public class BlockWhenQueueFull implements RejectedExecutionHandler {

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // The pool is full. Wait, then try again.
        try {
            long waitMs = 250;
            Thread.sleep(waitMs);
        } catch (InterruptedException interruptedException) {}

        executor.execute(r);
    }
}

Цей клас можна просто використовувати у виконанні пулових потоків як RejectedExecutinHandler, як і будь-який інший, наприклад:

executorPool = new ThreadPoolExecutor(1, 1, 10,
                                      TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
                                      new BlockWhenQueueFull());

Єдиний недолік, який я бачу, - це те, що виклична нитка може заблокуватися трохи довше, ніж суворо необхідно (до 250 мс). Більше того, оскільки цього виконавця ефективно викликають рекурсивно, дуже довге очікування появи потоку (години) може призвести до переповнення стека.

Тим не менш, мені особисто подобається цей метод. Це компактний, легкий для розуміння та добре працює.


1
Як ви самі кажете: це може створити потоковий процес. Не те, що я хотів би мати у виробничому коді.
Харальд

Кожен повинен приймати власні рішення. Для мого навантаження це не проблема. Завдання виконуються за секунди замість годин, які знадобляться для того, щоб підірвати стек. Більше того, те саме можна сказати практично для будь-якого рекурсивного алгоритму. Це причина ніколи не використовувати будь-який рекурсивний алгоритм у виробництві?
TinkerTank
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.