序文
スレッドプールの原理の分析については、「http://objcoding.com/2019/04/25/threadpool-running/」を参照してください。原理を理解していない子供用の靴は、まずこの記事を読んでから、この記事をご覧になることをお勧めします。スレッドプールについての私の理解についてお話しますが、何か問題があれば、批判して修正したいと思います。
スレッドプールの考え方
スレッドプールは、アプリケーションレベルのタスクを実行する準備ができている事前にインスタンス化されたスペアスレッドのセットと考えることができます。スレッドプールは、複数のタスクを同時に実行することでパフォーマンスを向上させると同時に、Webなどのスレッド作成中の時間とメモリのオーバーヘッドを防ぎますサーバーは起動時にスレッドプールをインスタンス化するので、クライアントが入力を要求したときに、スレッドの作成に時間を費やしません。各タスクのスレッドを作成する場合と比較して、スレッドプールはリソースを回避します(処理プロセッサ、カーネル、メモリなど)が使い果たされ、特定の数のスレッドが作成された後、追加のタスクは通常、新しいタスクに使用できるスレッドができるまで待機キューに入れられます。以下に、簡単な例を使用してスレッドプールの原理を要約します。
public static void main(String [] args){ ArrayBlockingQueue <Runnable> arrayBlockingQueue = new ArrayBlockingQueue <>(5 ); ThreadPoolExecutor poolExecutor = 新しい ThreadPoolExecutor(2 、 5 、はLong.MAX_VALUE、TimeUnit.NANOSECONDS、arrayBlockingQueue)。 for(int i = 0; i <11; i ++ ){ try { poolExecutor.execute(new Task()); } catch (RejectedExecutionException ex){ System.out.println( "拒绝任务=" +(i + 1)); } printStatus(i + 1 、poolExecutor); } } static void printStatus(int taskSubmitted、ThreadPoolExecutor e){ StringBuilder s = new StringBuilder(); s.append( "工作池大小=" ). append(e.getPoolSize()). append( "、核心池大小=" ). append(e.getCorePoolSize()). append( "、队列大小=" ) .append(e.getQueue()。size()) .append( "、队列残留余容量=") .append(e.getQueue()。remainingCapacity()). append( "、最大池大小=" ). append(e.getMaximumPoolSize()). append( "、提交任务数=" ). append(taskSubmitted); System.out.println(s.toString()); } 静的 クラス Task はRunnableを 実装します{ @Override public void run(){ while(true ){ try { Thread.sleep( 1000000 ); } catch (InterruptedException e){ break ; } } } }
上記の例はスレッドプールの基本原理を示しているため、境界キュー(容量は5)を宣言し、インスタンス化されたスレッドプールのコアプールサイズは2、最大プールサイズは10、スレッドを作成するためのカスタム実装はなく、デフォルトが渡されますスレッドプールファクトリが作成され、拒否ポリシーがデフォルトであり、11個のタスクが送信されます。スレッドプールを開始すると、デフォルトではスレッドなしで開始されます。最初のタスクを送信すると、最初のワーカースレッドが生成され、現在のワーカースレッド数が構成済みコアよりも少ない限り、タスクはこのスレッドに渡されます。プールサイズ。以前に作成されたコアスレッドの一部がアイドル状態の場合でも、新しく送信されたタスクごとに新しいワーカースレッドが生成されます(注:ワーカースレッドのプールサイズがコアプールのサイズを超えない場合、ワーカーが作成されます最初のタスクの実行はfirstTaskであり、ブロッキングキューはバイパスされます。コアプールのサイズを超えると、タスクはブロッキングキューに配置されます。ブロッキングキューがいっぱいになると、スレッドタスクが再作成されます。拒否戦略。ブロッキングキューが無制限のキュー(LinkedBlockingQueueなど)である場合、設定された最大プールサイズが無効になることは明らかです。もう一度詳しく説明しましょう。ワーカースレッドの数がコアプールのサイズに達したとき、この時点でより多くのタスクが送信された場合、スレッドプールの具体的な動作は何ですか?
1.アイドルコアスレッド(以前に作成されたが、割り当てられたタスクが完了したワーカースレッド)がある限り、送信された新しいタスクを引き継ぎ、実行します。
2.利用可能な空きコアスレッドがない場合、コアスレッドが処理できるまで、サブミットされた新しいタスクはそれぞれ、定義されたワークキューに入ります。ワークキューがいっぱいでも、タスクを処理するのに十分な空きコアスレッドがない場合は、スレッドプールが再開して新しいワーカースレッドが作成され、新しいタスクがそれらによって実行されます。ワーカースレッドの数が最大プールサイズに達すると、スレッドプールは再び新しいワーカースレッドの作成を停止し、この時間以降に送信されたすべてのタスクは拒否されます。
上記の2から、コアスレッドサイズに達すると、ブロッキングキューに入る(ブロッキングキューがいっぱいではない)ことがわかります。これは、ブロッキングキューの優先度を実行するメカニズムであると考えることができます。次に、コア以外のスレッドを作成しないでください。スレッドは、ブロッキングキューに入る代わりにスレッドプールのサイズを拡張します。最大プールサイズに達すると、ブロッキングキューがキューに入れられます。この方法とデフォルトの実装では、効率とパフォーマンスが向上しますか?しかし、別の見方をすると、すぐにブロッキングキューに入らないようにするために、指定されたコアプールサイズを大きくしてみませんか?スレッド数が多いほど、非ピークシステムのスレッド数が増えることはわかっています。つまり、ピークシステムでの非コアスレッドの作成は、理論的にはデフォルトよりもすぐにブロックできます。キューには、大規模なタスクをサポートするためのパフォーマンス上の利点がありますか?では、どうすればデフォルトの操作を変更できますか?まず、タスクを実行しながら操作を見てみましょう
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 ); } else if(!addWorker(command、false )) reject(command); } }
最初のステップでは、ワーカースレッドの現在の数がコアプールのサイズよりも小さい場合、コアプールに基づいてスレッドを作成し、タスクを実行します。問題はありません。2番目のステップでは、ワーカースレッドのサイズがコアプールのサイズを超えている場合、現在のスレッドが実行されています。ステータスとそのタスクをブロッキングキューに入れます。失敗した場合、3番目の手順は非コアプールスレッドを作成することです。ソースコード分析により、アイドルプールがある場合でも、コアプールのスレッドがスレッドを作成してタスクを実行する場合、コアプールにアイドルスレッドがあるかどうかを確認し、アイドルスレッドがある場合は、それらをブロッキングキューに入れようとするため、ブロッキングキューのofferメソッドを書き換え、アイドルコアプールを持つスレッドを追加して、タスクを受信できるようにする必要があります。したがって、上記の制限されたブロッキングキューを次のように継承します。
パブリック クラス CustomArrayBlockingQueue <E> extends ArrayBlockingQueue { private final AtomicInteger idleThreadCount = new AtomicInteger(); public CustomArrayBlockingQueue(int capacity){ super (capacity); } @Override public boolean offer(Object o){ return idleThreadCount.get()> 0 && super .offer(o); } }
ただし、残念ながら、スレッドプールのソースコードを分析しても、アイドル状態のコアプールのスレッドを取得することはできませんが、コアプール内のアイドル状態のスレッドを追跡することはできます。タスクを取得する方法は次のとおりです。
boolean timed = allowCoreThreadTimeOut || wc> corePoolSize; if((wc> maximumPoolSize ||(timed && timedOut)) &&(wc> 1 || workQueue.isEmpty())){ if (compareAndDecrementWorkerCount(c)) return null ; 続ける; } 試みる{ RunnableをR =時限? workQueue.poll(keepAliveTime、TimeUnit.NANOSECONDS): workQueue.take(); if(r!= null )は rを返します。 timedOut = true ; } キャッチ(InterruptedException再試行){ timedOut = false ; }
タスクのコアは上記のようにインターセプトされます。ワーカースレッドのサイズがコアプールのサイズよりも大きい場合、デフォルトでブロッキングキューに入ります。このとき、ブロッキングキューのタスクはプールを通じて取得されます。ワーカースレッドのサイズがコアプールのサイズより小さい場合、takeはこの時点で呼び出されます。このメソッドは、ブロッキングキューから使用可能なタスクを取得します。現時点では、現在のコアプールスレッドがアイドル状態であることを意味します。キューにタスクがない場合、スレッドは、使用可能なタスクがなくなるまでこの呼び出しでブロックされるため、コアプールスレッドはまだこれはアイドル状態なので、上記のカウンターを増やします。それ以外の場合は、callメソッドが戻ります。この時点で、スレッドはアイドル状態ではなくなります。カウンターを減らし、takeメソッドを次のように書き換えます。
@Override public Object take()throws InterruptedException { idleThreadCount.incrementAndGet(); オブジェクトtake = super .take(); idleThreadCount.decrementAndGet(); テイクを返す; }
次に、timedがtrueの場合を考えてみましょう。この場合、スレッドはポーリングメソッドを使用します。明らかに、ポーリングメソッドに入ったスレッドは現在アイドル状態であるため、作業キューでこのメソッドを書き換えることができます。最初にカウンターを増やすための実装。次に、実際のポーリングメソッドを呼び出すことができます。これにより、次の2つのケースが発生する可能性があります。キューにタスクがない場合、スレッドはこの呼び出しを待機して、提供されたタイムアウトを提供し、nullを返します。 。この時点で、スレッドはタイムアウトになり、すぐにプールから出て、アイドルスレッドの数が1つ減るので、この時点でカウンターを減らすことができます。そうしないと、メソッド呼び出しによってカウンターが返され、スレッドはアイドル状態ではなくなります。カウンターを減らすこともできます。
@Override public Object poll(long timeout、TimeUnit unit)throws InterruptedException { idleThreadCount.incrementAndGet(); オブジェクトのポーリング = super .poll(timeout、unit); idleThreadCount.decrementAndGet(); 投票を返す; }
上記のoffer、pool、takeメソッドの書き換えにより、コアプールに基づくアイドルスレッドのない非コアスレッドの拡張はまだ終わっていません。最大プールサイズに達した場合は、それらをブロッキングキューに追加する必要がありますキューなので、最終的には次のようにカスタムブロッキングキューを使用し、カスタム拒否戦略を使用します。
CustomArrayBlockingQueue <Runnable> arrayBlockingQueue = new CustomArrayBlockingQueue <>(5 ); ThreadPoolExecutor poolExecutor = 新しい ThreadPoolExecutor(10 、 100 、はLong.MAX_VALUE、TimeUnit.NANOSECONDS、arrayBlockingQueue 、Executors.defaultThreadFactory()、(R、エグゼキュータ) - > { なら!(。executor.getQueue()を追加(R)){ システム.out.println( "拒绝任务" ); } }); for(int i = 0; i <150; i ++ ){ 試行{ poolExecutor.execute(new Task()); } catch (RejectedExecutionException ex){ System.out.println( "拒绝任务=" +(i + 1 )); } printStatus(i + 1 、poolExecutor); }
上記では、カスタム拒否戦略を実装し、拒否されたタスクをブロッキングキューに入れています。ブロッキングキューがいっぱいになり、新しいタスクを受信できなくなった場合は、デフォルトの拒否戦略またはその他のハンドラーを呼び出すので、ブロッキングキューに追加するとき、つまりaddメソッドを呼び出すときは、addメソッドを次のように書き換える必要もあります。
@Override public boolean add(Object o){ return super .offer(o); }
まとめ
上記の詳細は、スレッドプールのデフォルト実装によって引き起こされる反射のみです。上記の方法で大規模タスクのパフォーマンスを改善できますか?ここで分析する間、よく考えられていないいくつかの領域があるかもしれません。