並行プログラミング(5):スレッドプールと基本原則の実装

1つ:スレッドプールとは

 

プーリング技術:毎回リソースの消費を減らし、リソースの利用を改善します。たとえば、スレッドプール、データベース接続プール、HTTP接続プールなどです。

スレッドプールを使用する利点:

  • リソース消費を減らします。すでに作成されたスレッドを再利用することにより、スレッドの作成と破棄による消費を削減します。
  • 応答速度を改善します。タスクが到着すると、スレッドが作成されるのを待たずにタスクをすぐに実行できます。
  • スレッドの管理性を向上させます。スレッドは希少なリソースです。無制限に作成すると、システムリソースが消費されるだけでなく、システムの安定性も低下するため、スレッドプールの使用は、割り当て、調整、監視を統合することができます。

 

2:RunnableインターフェースとCallableインターフェースの実装の違い

 

呼び出し可能インターフェース:スレッド実行結果が返されるか、チェック例外がスローされます。スレッド実行結果と例外検出は、呼び出し可能インターフェースを使用する必要があります。それ以外の場合は、実行可能インターフェース(コードはより簡潔です)を使用する必要があります。

RunnableはJava 1.0から存在していますが、CallableはJava 1.5でのみ導入されたものであり、Runnableがサポートしていない使用例に対処することを目的としています。Runnableインターフェイスは結果を返さないか、チェック例外をスローしますが、Callableインターフェイスは返します。したがって、タスクが結果を返したり、例外をスローしたりする必要がない場合は、Runnableインターフェイスを使用して、コードをより簡潔にすることをお勧めします。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     * 
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
...

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result 计算结果
     * @throws Exception if unable to compute a result 无法计算结果则抛出异常
     */
    V call() throws Exception;
}
...
// FutureTask类 对Callable进行了封装
public class FutureTask<V> implements RunnableFuture<V> {
    /** The underlying callable; nulled out after running */
    private Callable<V> callable;
    private static final int NEW          = 0;
    
    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

}

3:execute()メソッドとsubmit()メソッドの実行の違いは何ですか?

  1. execute()メソッド:戻り値を必要としないタスクを送信するために使用されるため、タスクがスレッドプールによって正常に実行されたかどうかを判断することはできません。
  2. submit()メソッド:戻り値を必要とするタスクを送信するために使用されます。スレッドプールは、Futureタイプのオブジェクトを返します。このFutureオブジェクトを通じて、タスクが正常に実行されたかどうかを判断でき、Futureのget()メソッドを通じて戻り値を取得できます。get()メソッドは、タスクが完了するまで現在のスレッドをブロックし、getを使用します。 (長いタイムアウト、TimeUnit単位)メソッドは、現在のスレッドをブロックし、一定の時間が経過するとすぐに戻ります。この時点では、完了していないタスクがある可能性があります。

送信方法:

// AbstractExecutorService 接口中的一个 submit 方法
public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
 }
 ...
 protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
 }

メソッドを実行:

// execute 方法
public void execute(Runnable command) {
      ...
 }

4:スレッドプールを作成する方法

 

「Alibaba Java開発マニュアル」では、必須のスレッドプールではExecutorを作成できませんが、ThreadPoolExecutorを使用して作成できます。

スレッドプールオブジェクトを返すエグゼキューターの欠点は次のとおりです。

  • FixedThreadPoolおよびSingleThreadExecutor:リクエストのキューの長さがInteger.MAX_VALUEになるようにします。これにより、多数のリクエストが蓄積され、OOMが発生する可能性があります。
  • CachedThreadPoolおよびScheduledThreadPool:作成できるスレッドの数はInteger.MAX_VALUEであり、これにより多数のスレッドが作成され、OOMが発生する可能性があります。

1:エグゼキューターフレームワークのエグゼキューターツールクラスを使用して、3種類のThreadPoolExecutorを作成できます。

  • FixedThreadPool:このメソッドは、固定数のスレッドを持つスレッドプールを返します。このスレッドプールのスレッド数は常に同じです。新しいタスクが送信されると、スレッドプールにアイドルスレッドがある場合、そのタスクはすぐに実行されます。そうでない場合、新しいタスクは一時的にタスクキューに格納され、スレッドがアイドル状態のときにタスクキュー内のタスクが処理されます。
  • SingleThreadExecutor:このメソッドは、スレッドが1つだけのスレッドプールを返します。スレッドプールに複数のタスクが送信された場合、タスクはタスクキューに保存され、スレッドがアイドルになるのを待って、キュー内のタスクを先入れ先出しの順序で実行します。
  • CachedThreadPool:このメソッドは、実際の状況に応じてスレッド数を調整できるスレッドプールを返します。スレッドプール内のスレッド数は不明ですが、再利用できるアイドルスレッドがある場合は、再利用可能なスレッドが優先されます。すべてのスレッドが機能し、新しいタスクが送信されると、タスクを処理するための新しいスレッドが作成されます。現在のタスクの実行が完了すると、すべてのスレッドが再利用のためにスレッドプールに戻ります。

2:ThreadPoolExecutor構築メソッド(すべてのパラメーター):

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters and default thread factory.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code handler} is null
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

重要なパラメータ:

corePoolSize:コアスレッドの数(同時に実行できるスレッドの最小数)

maximumPoolSize:キューに格納されているタスクがキューの容量に達すると、同時に実行できるスレッドの現在の数が最大スレッド数になります。

workQueue:新しいタスクが来ると、最初に、現在実行中のスレッドの数がコアスレッドの数に達したかどうかを判断し、達した場合、新しいタスクはキューに格納されます。

共通パラメーター:

keepAliveTime:スレッドプール内のスレッド数がcorePoolSizeよりも大きい場合、この時点で新しいタスクが送信されないと、コアスレッド外のスレッドはすぐには破棄されませんが、待機時間がkeepAliveTimeを超えるまで待機してから、リサイクルされて破棄されます。 ;

unit:keepAliveTimeパラメータの時間単位。

threadFactory:新しいスレッドを作成するときにexecutorが使用されます。

ハンドラー:飽和戦略。

 

 5:ThreadPoolExecutor飽和戦略

現在実行中のスレッドの数が最大スレッド数に達し、キューがすでにいっぱいである場合に使用される処理方法。

4つの飽和戦略:

  • ThreadPoolExecutor.AbortPolicy:新しいタスクの処理を拒否するには、RejectedExecutionExceptionをスローします。
  • ThreadPoolExecutor.CallerRunsPolicy:独自のスレッド実行タスクを実行するための呼び出し。リクエストは終了しません。しかし、この戦略は新しいタスクの提出速度を低下させ、プログラムの全体的なパフォーマンスに影響を与えます。さらに、この戦略はキュー容量を増やすことを好みます。アプリケーションがこの遅延を許容でき、タスク要求を破棄できない場合は、この戦略を選択できます。
  • ThreadPoolExecutor.DiscardPolicy:新しいタスクを処理せずに破棄します。
  • ThreadPoolExecutor.DiscardOldestPolicy:このポリシーは、最も古い未処理のタスク要求を破棄します。

6:単純なスレッドプールデモ:Runnable + ThreadPoolExecutor

public class DemoTest {
    private static int corePoolSize = 5;//核心线程数
    private static int maximumPoolSize = 15;//最大线程数
    private static long keepAliveTime = 1l;//线程未能获取最大请求时间,超过则销毁
    private static BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);//线程存放队列

    static class MyRunnable implements Runnable {

        private String name;
        public MyRunnable(String name){
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"开始--->"+System.currentTimeMillis());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"结束--->"+System.currentTimeMillis());
        }
    }

    public static void main(String[] args){
        ThreadPoolExecutor threadPoolExecutor = 
        new ThreadPoolExecutor(corePoolSize,maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                workQueue,new ThreadPoolExecutor.CallerRunsPolicy());
                //线程饱和策略
        for (int i = 0; i < 15; i++) {
            Runnable myRunnable = new MyRunnable(""+i);
            // 线程池中调用线程
            threadPoolExecutor.execute(myRunnable);
        }
        // 终止线程池
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()){

        }
        System.out.println("Finished all threads");
    }
}
  1. corePoolSize:コアスレッドの数は5です。
  2. maximumPoolSize:スレッドの最大数10
  3. keepAliveTime:待機時間は1Lです。
  4. unit:待ち時間の単位はTimeUnit.SECONDSです。
  5. workQueue:タスクキューはArrayBlockingQueueで、容量は100です。
  6. ハンドラー:飽和戦略はCallerRunsPolicyです

コアスレッドの数を設定する方法:

  • CPU集中タスク(N + 1):このタスクは主にCPUリソースを消費します。スレッドの数をN(CPUコアの数)+1に設定できます。CPUコアの数よりも1つ多いスレッドは、散発的なスレッドを防ぐためです。ページ違反の中断、またはタスクの中断によって引き起こされたその他の理由。タスクが一時停止されると、CPUはアイドル状態になり、この場合、追加のスレッドがCPUのアイドル時間を最大限に活用できます。
  • I / O集中型タスク(2N):このタスクが適用されると、システムはほとんどの時間をI / Oインタラクションの処理に費やし、スレッドは期間中I / Oを処理するためにCPUを使用しません。 CPUを他のスレッドに渡すことができます。したがって、I / O集中型タスクのアプリケーションでは、より多くのスレッドを構成できます。具体的な計算方法は2Nです。

7:スレッドプールの原理分析

ソースコードの分析は次のとおりです。

// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static int workerCountOf(int c) {
    return c & CAPACITY;
}

private final BlockingQueue<Runnable> workQueue;

public void execute(Runnable command) {
    // 如果任务为null,则抛出异常。
    if (command == null)
        throw new NullPointerException();
    // ctl 中保存的线程池当前的一些状态信息
    int c = ctl.get();

    //  下面会涉及到 3 步 操作
    // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
    // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
    // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
        if (!isRunning(recheck) && remove(command))
            reject(command);
            // 如果当前线程池为空就新创建一个线程并执行。
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
    else if (!addWorker(command, false))
        reject(command);
}

8:いくつかの一般的なスレッドプールの詳細な説明

 

 FixedThreadPoolは、固定数のスレッドプールを再利用できます。

  1. 現在実行中のスレッドの数がcorePoolSize未満の場合、新しいタスクが再び来ると、タスクを実行するための新しいスレッドが作成されます。
  2. 現在実行中のスレッドの数がcorePoolSizeと等しくなった後、新しいタスクが来ると、そのタスクはLinkedBlockingQueueに追加されます。
  3. スレッドプール内のスレッドが手元のタスクを実行した後、実行するループ内のLinkedBlockingQueueからタスクを繰り返し取得します。

短所:

FixedThreadPoolは、無制限のキューLinkedBlockingQueue(キューの容量はIntger.MAX_VALUE)をスレッドプールの作業キューとして使用します。これにより、スレッドプールに次のような影響があります。

  1. スレッドプール内のスレッド数がcorePoolSizeに達すると、新しいタスクは無制限のキューで待機するため、スレッドプール内のスレッド数はcorePoolSizeを超えません。
  2. タスクキューをいっぱいにすることは不可能であるため、無制限のキューを使用する場合、maximumPoolSizeは無効なパラメーターになるためです。したがって、FixedThreadPoolのソースコードを作成すると、作成されたFixedThreadPoolのcorePoolSizeとmaximumPoolSizeが同じ値に設定されていることがわかります。
  3. 1と2のため、無制限のキューを使用する場合、keepAliveTimeは無効なパラメーターになります。
  4. FixedThreadPool(shutdown()またはshutdownNow()を実行していない)の操作はタスクを拒否せず、さらにタスクがある場合にOOM(メモリオーバーフロー)を引き起こします。

 

スレッドが1つだけのSingleThreadExecutorスレッドプール

SingleThreadExecutorは、無制限のキューLinkedBlockingQueueをスレッドプールの作業キューとして使用します(キューの容量はIntger.MAX_VALUEです)。SingleThreadExecutorは、スレッドプールのワークキューがFixedThreadPoolと同じ影響をスレッドプールに与えるため、無制限のキューを使用します。簡単に言えば、OOMを引き起こす可能性があります。

 

CachedThreadPoolは、必要に応じて新しいスレッドを作成するスレッドプールです。

CachedThreadPoolによって許可されるスレッドの数はInteger.MAX_VALUEであり、これにより多数のスレッドが作成され、OOMが発生する可能性があります。

 

ScheduledThreadPool scheduleThreadPoolExecutorは主に、指定された遅延後にタスクを実行するため、または定期的にタスクを実行するために使用されます

ScheduledThreadPoolExecutor DelayQueue(遅延キュー)によって使用されるタスクキューはPriorityQueueをカプセル化し、PriorityQueue(優先度キュー)はキュー内のタスクをソートします。実行時間は短く、最初に実行されます(ScheduledFutureTask時間変数が小さく、最初の実行) )、実行時間が同じ場合、最初に送信されたタスクが最初に実行されます(squenceNumber変数が小さいScheduledFutureTaskが最初に実行されます)。

 

ScheduledThreadPoolExecutorとタイマーの比較:

  • タイマーはシステムクロックの変化に敏感ですが、ScheduledThreadPoolExecutorは敏感ではありません。
  • タイマーには実行のスレッドが1つしかないため、実行時間が長いタスクは他のタスクを遅延させる可能性があります。ScheduledThreadPoolExecutorは、任意の数のスレッドを構成できます。さらに、必要に応じて(ThreadFactoryを提供することにより)、作成されたスレッドを完全に制御できます。
  • TimerTaskでスローされたランタイム例外はスレッドを強制終了し、Timerをクラッシュさせます:-( ...スケジュールされたタスクは実行されなくなります。ScheduledThreadExecutorはランタイム例外をキャッチするだけでなく、必要に応じてそれらを処理することもできます(経由afterExecuteメソッドThreadPoolExecutorをオーバーライドします)。例外をスローするタスクはキャンセルされますが、他のタスクは引き続き実行されます。

親愛なる視聴者の皆さん、コメントへのいいね、ようこそ、一緒に議論して学んでください。ブロガーは、並行プログラミングに関する関連知識もすぐに更新します。同時に、ブロガーはベテランファンでもあり、プログラミング、2次元、感謝の気持ちを込めて、ずっと良くなっています。

 

 

元の記事を28件公開しました 賞賛されました0 訪問9931

おすすめ

転載: blog.csdn.net/weixin_38246518/article/details/105590892