【Java勉強ノート・並行プログラミング】スレッドプール

序文

前回の記事では、Java スレッドとタスクの概念を整理しました. この記事では Java スレッド プールについて説明します. スレッド プールをよりよく理解すると同時に、いくつかの設計上のアイデアも追加します.

ポータル:

【Java学習ノート・基礎編】Javaマルチスレッドについて - スレッドとタスク

1. Javaスレッドプールの登場

同時実行シナリオのパフォーマンスを最適化する必要がある場合、どこから始めればよいでしょうか? 明らかに、最も単純な角度:スレッドの作成と破棄のオーバーヘッドを節約します

IO 集中型のアプリケーションであろうと、コンピューティング集中型のアプリケーションであろうと、スレッドの管理にはコストがかかるため、コンピューターの観点からは、スレッドの数も制限するスレッドを作成する目的は、アプリケーションのパフォーマンスを向上させることであり、中断されたスレッドが多すぎると、スレッド管理のコストが高くなりすぎて、アプリケーションのパフォーマンスも低下します。

上記の 2 つの要件のために、スレッド プールが誕生しました。

まず、この記事のインターフェイスと実装クラスの関係を示します。

ここに画像の説明を挿入

次に、Java スレッド プールの基本的なインターフェイス

Executor と ExecutorService

これは 2 つのスレッド プールの基本的なインターフェイスであり、ExecutorService は Executor を継承します。一般に、ExecutorService の方が仕様がより完全であるため、ExecutorService を使用する実装が多くあります。まず、次の 2 つのインターフェイスを見てください。

public interface Executor {
    
    

    void execute(Runnable command);
    
}

Runnable インターフェースと同様に、定義は非常に単純で、タスクの実装をエグゼキューターに入れ、タスクを実行します。

public interface ExecutorService extends Executor {
    
    

	//关闭控制类函数
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

	//运行类函数
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    Future<?> submit(Runnable task);
	
	//批量运行类函数
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

ここでは、スレッド プールのプロトタイプを見ることができます.スレッドをより適切に管理するために、スレッド プールに必要な基本的な機能を実現できます。

ExecutorService には Executor よりも多くの仕様がありますが、要約は次の 3 つのカテゴリにすぎません。

  • シャットダウンおよびその他の制御機能 (プール レベル)
  • クラス関数の実行 (スレッドレベル)
  • 一括操作型機能(第2型の拡張)

ここでは、次の 2 つのインターフェイスに注意を払うことができます。

    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);

見覚えがありますか? 最初にここのボタンを押してください。次のセクションでなぜこのように設計されているかがわかります。

3. Javaスレッドプールの基本実装

AbstractExecutorService

この抽象クラスは ExecutorService インターフェースを段階的に実装します. このクラスでは、スレッドプールを実装するためのいくつかのアイデアと手順が明確にされています.

まず、タスクの基本的な実装と単一のタスクの実行を見てみましょう。

  • スレッド プールの基本的なタスク タイプは FutureTask です。
  • タスク関数を送信するには、新しいタスク オブジェクトを作成する -> エグゼキューターがタスクを実行するという 2 つの手順が必要です。
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    
    
        return new FutureTask<T>(runnable, value);
    }

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    
    
        return new FutureTask<T>(callable);
    }

    public Future<?> submit(Runnable task) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    public <T> Future<T> submit(Runnable task, T result) {
    
    
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

これまでのところ、submit 関数がよく知られているように見える理由は理解できました。これは、FutureTask と同じロジックに従っており、2 つのきちんとしたインターフェイスも提供しているためです。

次にバッチ操作の処理方法を見てみましょう.invokeAnyはスレッド管理のスケジューリングを含みます.ここではまず表を押してみましょう.まずinvokeAllでの実装を見てみましょう.

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
    
    
        if (tasks == null)
            throw new NullPointerException();
        ArrayList<Future<T>> futures = new ArrayList<>(tasks.size());
        try {
    
    
            for (Callable<T> t : tasks) {
    
    
                RunnableFuture<T> f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++) {
    
    
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
    
    
                    try {
    
     f.get(); }
                    catch (CancellationException | ExecutionException ignore) {
    
    }
                }
            }
            return futures;
        } catch (Throwable t) {
    
    
            cancelAll(futures);
            throw t;
        }
    }

値のフェッチはリスト全体をトラバースするプロセスであることがわかります。これは、結果がリストの順序で取得されるため、完了時間が最も遅い

ここまでで、スレッド プールの予備的な理解ができました。Java が完全なスレッド プールを実装する方法を見てみましょう。

ThreadPoolExecutor

まず、スレッド プールの初期化に必要な最小限の要素を構築メソッドで確認できます。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
    
    
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

パラメータの説明:

  • corePoolSize: スレッド プール内のコア スレッドの数。タスクを追加するときに、現在のスレッド数 (アイドル + 非アイドル) が corePoolSize 未満の場合、現在アイドル状態のスレッドがあっても、タスクを実行するために新しいスレッドが作成されます。現在のスレッド数 (アイドル + 非アイドル) が corePoolSize と等しい場合、それ以上のスレッドは再作成されません。
  • maximumPoolSize: スレッド プール内のスレッドの最大数。ブロッキング キューがいっぱいで、現在のスレッド数 (アイドル + 非アイドル) が maximumPoolSize 以下の場合、タスクを実行するために新しいスレッドが作成されます。
  • keepAliveTime: 現在のスレッド数 (アイドル + 非アイドル) が corePoolSize よりも大きく、アイドル時間が keepAliveTime よりも大きい場合、これらのアイドル スレッドは破棄され、リソースが解放されます。
  • unit: 時間の単位。
  • workQueue: ブロッキング キューのインスタンス。
  • ThreadFactory: スレッド ファクトリ クラスで、通常はデフォルトのファクトリを使用します。
  • RejectedExecutionHandler: 飽和戦略。タスクが多すぎる場合、タスクの処理戦略。
1. スレッドプールの状態 - ビット操作

操作の効率を向上させ、オーバーヘッドを削減するために、いくつかの属性値を記録するときに、ビット操作、つまりビット操作を使用します。(例えばアルゴリズムクラスでハフマン木を利用して圧縮を実現したい場合、ソースファイルの各種属性データを記録する必要があります)。

ビット操作などの概念が明確でない場合は、次の場所に移動できます。

javaでの
ビット操作とシフト操作の詳細解説

ThreadPollExecutor が int を使用して 2 つの属性を記録する方法を見てみましょう。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //能存放 32-3 位的活跃线程数量。
    private static final int COUNT_BITS = Integer.SIZE - 3;
    // 
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits(3 bits free)
    // 在高位存储线程池状态
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    private static int runStateOf(int c)     {
    
     return c & ~COUNT_MASK; }
    private static int workerCountOf(int c)  {
    
     return c & COUNT_MASK; }
    private static int ctlOf(int rs, int wc) {
    
     return rs | wc; }
2. 生産消費モデル - BlockingQueue (ブロッキングキュー)

ThreadPoolExecutor では、保存タスクを管理するデータ構造がブロッキング キューです。その中で、生産の仕方は提供であり、消費の仕方は世論調査です。

public interface BlockingQueue<E> extends Queue<E> {
    
    

	//……………………

    boolean offer(E e);

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
	//如果取出元素后,队列为空,一直阻塞等待。
    E take() throws InterruptedException;
    //如果取出元素后,队列为空,则阻塞超时后返回null。
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

	//……………………
	
}

インターフェイスの仕様では、poll と take の違いが簡単にわかります。スレッド プール内のアプリケーションを見てみましょう。

    public void execute(Runnable command) {
    
    
		//……………………
        //向队列生产任务
        if (isRunning(c) && workQueue.offer(command)) {
    
    
            //……………………
        }
        //……………………
    }

    private Runnable getTask() {
    
    
			//……………………
			boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
            try {
    
    
            //依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
                Runnable r = timed ?
                	//如果queue is null,即没有可执行任务,则阻塞超时后 return null。
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
    
    
                timedOut = false;
            }
        }
    }

ヒント:
要素に timed 属性があるかどうかに基づいて、タスクを消費する方法を選択します。
ポーリング消費方式の場合、要素に timeout 属性があり (詳細は BlockingQueue のソース コードを参照)、タイムアウト (アイドル) 時間になるまでその値でブロックし、null を返します。 ) 関数で、スレッド破棄メソッド processWorkerExit を呼び出して、次のことを達成します。
take consumer メソッドは常にブロックして待機しますが、getTask() の場合は常に戻り値があるため、スレッドは破棄されません。達成するために:スレッド数<= corePoolSizeの場合、アイドルスレッドはリサイクルされません。

3. タスク実行者 - ワーカー

ThreadPoolExecutor には Worker の内部クラスがあります。簡単に言えば、それはタスクのコンテナー (runnable の実装 - タスク) であり、スレッドとして大まかに理解できますが、まったく同じではありません。

ワーカーとは

実際、Worker はタスク コンテナーであり、その作成方法は非常に単純です。

    private final class Worker extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
        Worker(Runnable firstTask) {
    
    
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
    }

新しいスレッド オブジェクトを作成し、それにタスクを追加します。

Executor が Worker を管理する方法

まず、Executor は、各ワーカーの一意性を確保するためにセットを通じてワーカーを保存します。

	private final HashSet<Worker> workers = new HashSet<>();

次に、スレッド プールで最も重要なのは addWorker と runWorker です。ここではソース コードを掲載しませんが、これら 2 つの関数の違いと使用法について簡単に説明します。

addWorker() 関数は、Worker インスタンスが実際に。これは、thread.start() 関数がその中にあるためです。少し調べてみると、addWorker が execute() 関数で呼び出されていることがわかります。そのため、スレッド プールがタスクを実行したい場合は、execute() 関数を呼び出す必要があることがわかります。

runWorker() 関数は、実際には Worker の実行です (補足説明)。

4. 実行機能 - 実行

ここのコードは、スレッド数が異なる場合のスレッド プール スレッド プールの処理戦略を示しています。

ここに画像の説明を挿入
上の写真は https://thinkwon.blog.csdn.net/article/details/102541900 からの引用です。

    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) && workQueue.offer(command)) {
    
    
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }

		//阻塞队列满,新建线程执行任务知道 max,如果创建失败或任务超过max,则启用饱和策略——中断。
        else if (!addWorker(command, false))
            reject(command);
    }
5. 飽和戦略 - ハンドラー

飽和戦略は、スレッドが異常でスレッド数がいっぱいになったときに現在のタスクを処理するために使用されます。少しは理解できます。

  • AbortPolicy: 送信されたタスクの実行を中止し、RejectedExecutionException をスローします。これは、スレッド プールの既定のポリシーです。
    	//默认饱和策略
    	private static final RejectedExecutionHandler defaultHandler =
        	new AbortPolicy();

		//实现在池子中的内部类
	    public static class AbortPolicy implements RejectedExecutionHandler {
    
    

        public AbortPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  • CallerRunsPolicy: 呼び出し元のスレッドを使用してタスクを実行します。つまり、呼び出し元から見ると、このタスクは呼び出し元のメイン スレッドでシリアル化されます。
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
    
    

        public CallerRunsPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                r.run();
            }
        }
    }
  • DiscardPolicy: タスクを処理せずに直接破棄します (空の関数を実行します)。
    public static class DiscardPolicy implements RejectedExecutionHandler {
    
    
    
        public DiscardPolicy() {
    
     }
        
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        
        }
    }
  • DiscardOldestPolicy: ブロッキング キューの先頭タスク (つまり、保存時間が最も長いタスク) を破棄し、現在のタスクを実行します。
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    
    

        public DiscardOldestPolicy() {
    
     }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
            if (!e.isShutdown()) {
    
    
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

ヒント:
実際、Java の飽和戦略は導入と見なすことができ、ビジネス シナリオでは、特定のビジネス機能の飽和戦略をカスタマイズできます。それが鍵です。

4. スレッドプールを使用する理由

上記の理解の後、スレッドプールの価値を要約できます。

  • リソース消費を削減します。既存のスレッドを再利用することにより、スレッドの作成と破棄のリソース消費を削減します。
  • スレッドの管理性を向上させます。スレッドは限られたリソースであり、無制限に増やすことはできません. スレッドが多すぎると、コンピューターにも負担がかかります.

おすすめ

転載: blog.csdn.net/weixin_43742184/article/details/113736775