飽和している場合にThreadPoolExecutorのsubmit()メソッドをブロックする方法は?


102

ThreadPoolExecutor最大サイズに達してキューがいっぱいになると、新しいタスクを追加しようとするとsubmit()メソッドがブロックするような方法を作成したいと思います。RejectedExecutionHandlerそのためのカスタムを実装する必要がありますか、それとも標準のJavaライブラリを使用してこれを行う既存の方法はありますか?




2
@bacar同意しない。このQ&Aは(古いことに加えて)より価値があります。
JasonMArcher、2014

回答:


47

私が見つけた可能な解決策の1つ:

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
ここで、finally節でセマフォが解放されてからセマフォが取得されるまでの間に競合状態はありますか?
volni

2
前述のように、タスクが完了する前にセマフォが解放されるため、この実装には欠陥があります。メソッドjava.util.concurrent.ThreadPoolExecutor#afterExecute(Runnable、Throwable)を使用することをお勧めします
FelixM 2013年

2
@FelixM:java.util.concurrent.ThreadPoolExecutor#runWorker(Worker w)のtask.run()の直後にafterExecuteが呼び出されるため、java.util.concurrent.ThreadPoolExecutor#afterExecute(Runnable、Throwable)を使用しても問題は解決されません。キューから次の要素を取得します(openjdk 1.7.0.6のソースコードを調べます)。
Jaan

1
この答えは、ブライアンゲッツ著「Java Concurrency in Practice」の本にあります
orangepips 2013

11
この答えは完全に正しいわけではなく、コメントも正しいです。このコードは、実際にはJava Concurrency in Practiceに由来するものであり、そのコンテキストを考慮に入れると適切です。この本は文字通り、「そのようなアプローチでは、無制限のキュー(...)を使用し、セマフォの境界をプールサイズと許可したいキューに入れられたタスクの数を足したものに等しくなるように設定します」と明記しています。無制限のキューを使用すると、タスクが拒否されることはないため、例外を再スローしてもまったく役に立ちません。どの理由も、私は信じている、あるthrow e;ある、NOT本に。JCIPは正解です。
Timmos

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

これは、非常にうまく機能する、めちゃくちゃ速くて簡単な実装ソリューションでした!
2012

58
これにより、送信スレッドで拒否されたタスクが実行されます。これは機能的にOPの要件を満たしていません。
知覚

4
これは、ブロックする代わりに「呼び出しスレッドで」タスクを実行してキューに配置します。これは、複数のスレッドがこの方法で呼び出すと、「キューサイズ」以上のジョブが実行され、タスクが予想よりも長くかかる場合、「生成中」のスレッドがエグゼキューターをビジー状態にしない可能性があります。しかし、ここでうまくいきました!
rogerdpack 2015

4
反対投票:TPEが飽和状態になってもブロックされません。これは単なる代替策であり、解決策ではありません。
Timmos

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のRejectedExecutionHandler.rejectedExecution(java.lang.Runnable、java.util.concurrent.ThreadPoolExecutor)メソッドを呼び出します。4つの定義済みハンドラーポリシーが提供されます。

  1. デフォルトのThreadPoolExecutor.AbortPolicyでは、ハンドラーは拒否時にランタイムRejectedExecutionExceptionをスローします。
  2. ThreadPoolExecutor.CallerRunsPolicyでは、execute自体を呼び出すスレッドがタスクを実行します。これにより、新しいタスクが送信される速度を遅くする単純なフィードバック制御メカニズムが提供されます。
  3. ThreadPoolExecutor.DiscardPolicyでは、実行できないタスクは単に削除されます。
  4. ThreadPoolExecutor.DiscardOldestPolicyで、エグゼキュータがシャットダウンされていない場合、作業キューの先頭にあるタスクが削除され、実行が再試行されます(これが再び失敗し、これが繰り返される可能性があります)。

また、ThreadPoolExecutorコンストラクターを呼び出すときは、必ずArrayBlockingQueueなどの境界キューを使用してください。そうでなければ、何も拒否されません。

編集:コメントに応じて、ArrayBlockingQueueのサイズをスレッドプールの最大サイズと等しくなるように設定し、AbortPolicyを使用します。

編集2:わかりました、私はあなたが何を手に入れているのかわかります。これについてはどうですか?beforeExecute()メソッドをオーバーライドして、をgetActiveCount()超えていないことを確認し、超えgetMaximumPoolSize()ている場合は、スリープしてから再試行しますか?


3
同時に実行されるタスクの数を(Executor内のスレッドの数によって)厳密に制限したいので、呼び出し元のスレッドがこれらのタスクを自分で実行できないようにします。
Fixpoint

1
AbortPolicyにより、呼び出し側のスレッドがRejectedExecutionExceptionを受け取るようになりますが、ブロックする必要があるだけです。
Fixpoint

2
私はスリープとポーリングではなく、ブロッキングを求めています;)
Fixpoint

@danben:CallerRunsPolicyを意味するのですか?
user359996 2011

7
CallerRunPolicyの問題は、単一のスレッドプロデューサーがある場合、長時間実行されているタスクが拒否された場合にスレッドが使用されないことがよくあることです(長時間実行されているタスクがまだある間にスレッドプールの他のタスクが終了するためです)ランニング)。
Adam Gent

6

HibernateにBlockPolicyはシンプルながあり、必要な処理を実行できます。

参照:Executors.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/...
ネイト・マレー

また、OPの要求に従って、「標準Javaライブラリ」を使用していません。削除しますか?
user359996 2011

1
うわー、それはとても醜いです。基本的に、このソリューションはTPEの内部に干渉します。javadoc ThreadPoolExecutorは文字通り、「メソッドgetQueue()を使用すると、監視とデバッグのためにワークキューにアクセスできます。このメソッドを他の目的で使用することはお勧めしません。」これが広く知られているライブラリで利用できることは、本当に悲しいことです。
Timmos

1
com.amazonaws.services.simpleworkflow.flow.worker.BlockCallerPolicyも同様です。
エイドリアン・ベイカー

6

BoundedExecutor上に引用された答えの練習でのJava並行処理を使用すると、エグゼキュータのためにアンバウンド形式のキューを使用する場合にのみ正しく機能、または拘束セマフォは、キューのサイズよりも大きくありません。セマフォはサブミットスレッドとプール内のスレッド間で共有される状態であり、キューサイズ<バインド<=(キューサイズ+プールサイズ)の場合でもエグゼキュータを飽和させることができます。

使用CallerRunsPolicyは、タスクが永久に実行されない場合にのみ有効です。その場合、送信スレッドはrejectedExecution永久に残ります。送信スレッドは新しいタスクを送信できないため、タスクの実行に長い時間がかかる場合はお勧めできません。それ自体がタスクを実行している場合は、他のことを行います。

それが許容できない場合は、タスクを送信する前に、エグゼキュータの境界キューのサイズを確認することをお勧めします。キューがいっぱいの場合は、しばらく待ってから、送信を再試行してください。スループットは低下しますが、他の多くの提案されたソリューションよりも単純なソリューションであり、タスクが拒否されないことが保証されます。


複数のタスクプロデューサーがいるマルチスレッド環境で、送信前にキューの長さをチェックして、拒否されたタスクがないことを保証する方法がわかりません。これはスレッドセーフではありません。
Tim

5

私は知っている、それはハックですが、私の意見では、ここで提供されたものの中で最もクリーンなハックです;-)

ThreadPoolExecutorは「put」ではなく「offer」のブロッキングキューを使用するため、ブロッキングキューの「offer」の動作をオーバーライドします。

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を参照してください。私は同意します、それはそれを行うための最もクリーンな方法です。
2015年

3

次のクラスはThreadPoolExecutorをラップし、セマフォを使用してブロックし、作業キューがいっぱいになります。

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

このラッパークラスは、Brian Goetz著のJava Concurrency in Practiceで提供されているソリューションに基づいています。本のソリューションはExecutor、セマフォに使用されるan とboundの2つのコンストラクタパラメータのみを受け取ります。これは、Fixpointの回答に示されています。そのアプローチには問題があります。プールスレッドがビジーで、キューがいっぱいですが、セマフォが許可を解放したばかりの状態になる可能性があります。(semaphore.release()finallyブロック内)。この状態では、新しいタスクは解放されたばかりの許可を取得できますが、タスクキューがいっぱいであるため拒否されます。もちろん、これはあなたが望むものではありません。この場合はブロックします。

これを解決するには、JCiPが明確に述べているように、無制限のキューを使用する必要があります。セマフォはガードとして機能し、仮想キューサイズの効果を与えます。これには、ユニットにmaxPoolSize + virtualQueueSize + maxPoolSizeタスクを含めることができるという副作用があります。何故ですか?semaphore.release()finallyブロック内にあるためです 。すべてのプールスレッドがこのステートメントを同時に呼び出すと、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()のドキュメントには、タスクキューへのアクセスは主にデバッグとモニタリングを目的として
Chadi

0

エグゼキューターが使用する独自のブロッキングキューを作成し、必要なブロッキング動作を使用しながら、常に利用可能な残りの容量を返します(エグゼキューターがコアプールより多くのスレッドを作成したり、拒否ハンドラーをトリガーしたりしないようにします)。

私はこれがあなたが探しているブロッキング行動を手に入れると信じています。拒否ハンドラは、実行者がタスクを実行できないことを示すため、請求書に適合しません。私が想像できることは、ハンドラーでなんらかの形の「ビジー待機」が発生することです。それはあなたが望むものではありません、あなたは発信者をブロックするエグゼキューターのためのキューが欲しいです...


2
ThreadPoolExecutorofferメソッドを使用してタスクをキューに追加します。BlockingQueueでブロックするカスタムを作成すると、契約offerが破られBlockingQueueます。
フィックスポイント

@Shooshpanchick、それはBlockingQueuesコントラクトを壊します。だから何?非常に熱心な場合は、submit()の間のみ明示的に動作を有効にできます(ThreadLocalが使用されます)
bestsss

この選択肢を詳しく説明する別の質問に対するこの回答も参照してください。
Robert Tupelo-Schneck 2014年

(ブロッキングバージョン)ではなくThreadPoolExecutor使用するように実装された理由はありますか?また、クライアントコードがどのコードをいつ使用するかを指示する方法があった場合、カスタムソリューションをハンドロールしようとする多くの人々は安心しましたofferput
asgs

0

@FixPointソリューションの問題を回避するため。ListeningExecutorServiceを使用して、FutureCallback内のセマフォonSuccessおよびonFailureを解放できます。


これには、Runnable通常のワーカークリーンアップの前にこれらのメソッドが呼び出されるため、単にラップするのと同じ固有の問題がありますThreadPoolExecutor。つまり、拒否の例外を処理する必要があります。
Adam Gent

0

最近、この問題に同じ問題があることがわかりました。OPは明示的に言っていませんがRejectedExecutionHandler、サブミッターのスレッドでタスクを実行するを使用したくありません。これは、このタスクが長時間実行されている場合、ワーカースレッドの使用率が低下するためです。

すべての回答とコメント、特にセマフォの問題のあるソリューションを使用するか、afterExecute私はThreadPoolExecutorのコードを詳しく調べて、解決策があるかどうかを確認しました。2000行以上の(コメントされた)コードがあり、その一部がめまいを感じさせることに驚いていました。私が実際に持っているかなり単純な要件を考えると、1つのプロデューサーと複数のコンシューマーがあり、コンシューマーが作業を実行できないときにプロデューサーをブロックさせます-私は自分のソリューションを導入することにしました。それはではなく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。新しいエグゼキューターサービスは、実行するスレッドがある場合にのみ新しいタスクを実行します。ブロッキングはセマフォによって管理され、許可の数はスレッドの数と同じです。タスクが完了すると、タスクは許可を返します。

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

私はJava Concurrency in Practiceで説明されているBoundedExecutorを実装し、セマフォの許可が要求の順序で提供されるようにするために、セマフォを公平性フラグをtrueに設定して初期化する必要があることを理解しました。docs.oracle.com/javase/7/docs/api/java/util/concurrent/…を参照してください。詳細
Prahalad Deshpande 2015

0

過去にも同じニーズがありました。共有スレッドプールに支えられた、各クライアントのサイズが固定された一種のブロッキングキューです。私は自分の種類のThreadPoolExecutorを作成することになりました。

UserThreadPoolExecutor(ブロッキングキュー(クライアントごと)+スレッドプール(すべてのクライアント間で共有))

参照:https : //github.com/d4rxh4wx/UserThreadPoolExecutor

各UserThreadPoolExecutorには、共有ThreadPoolExecutorからスレッドの最大数が与えられます

各UserThreadPoolExecutorは次のことができます。

  • クォータに達していない場合は、共有スレッドプールエグゼキュータにタスクを送信します。クォータに達すると、ジョブはキューに入れられます(CPUを待機する非消費ブロック)。送信されたタスクの1つが完了すると、割り当てが減少し、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はモニカを

@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
あなたが言うように:これはスタックオーバーフローを引き起こす可能性があります。量産コードに入れておきたいものではありません。
Harald

誰もが自分で決断するべきです。私のワークロードでは、これは問題ではありません。タスクは、スタックを爆破するのに必要な時間ではなく、数秒で実行されます。さらに、ほぼすべての再帰的アルゴリズムについても同じことが言えます。本番環境で再帰アルゴリズムを使用しないのはそのためですか?
TinkerTank 2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.