JAva同時学習の2:スレッドプール(5)


以前のスレッドプールでは、各スレッドを実行する基本的な機能が実装されており、テストしてみると、ThreadPoolExecutorの約1.5倍の速度です(もちろん、十分な理由がありますが、後述します)
その後のバージョンでは、「優雅な終了」と最適化(ブロックされていない)アイドルスレッドキューを実装する準備ができています.この手順は長い間考えていましたが、準備された実装方法も含めて多くの問題が見つかりました.
1.いくつかの方法を初歩的に考案した
  • void shutDown():このメソッドは、プールがタスクを受け入れなくなりますが、既存のタスクがすべて実行された後、
  • を停止します.
  • ListshutDownNow():このメソッドはinterruptスレッドを作成し、呼び出されていないタスクを収集し、呼び出されたタスクをさらに回収します(「呼び出された」は実行中のものを含みません.実行中に法人が停止していないため、スレッドに送信されたが、スレッドがまだ実行されていないもののみを含みます).その後、これらのタスクを
  • に戻します.
  • ListshutDownAndWait():shutDownNowと同様に、スレッドプールが完全に閉じるまで待つのは
  • です.
    2.スレッドとプールの状態は、最初はbooleanで実現されたが、例えばisShutDown、isRunningなど、これらの変数はvolatileと宣言されることを避けることができず、volatileの書き込みと読み取りはスレッドの内部変数へのアクセスよりも多くの消費を占め、複数のbooleanを使うよりもrunStateを1つ使うほうがいいことを知っている.サイズを判断するだけでいいのですが、volatile変数へのアクセスは一度しかありません.サイズを判断するのにかかるcpu時間はほとんど無視できます.
    これも原則的な問題です.ここに記録しておくと、蓄積されます.
    1つのものの状態(複数の状態)を表し、できるだけintを使い、同期の問題があればvolatile、CASで単一変数の制御に有効です
    3.すでに発行されているが、実行されていないタスクをどのように回収しますか?これはもう一つの問題に関連しており、スレッドを取得する必要がある状態(タスクの実行状態であってもよい.ここでは未実行のタスクを回収することを目標としているため)は、より多くのvolatile変数を導入せざるを得ない.そして、この状態変数は、タスクを実行するたびに修正しなければならず、大きな消費になる(これは容認できない)
    注意:ここではThread.getState()を使用してスレッドの状態を取得することはできません.この状態はシステムで監視され、論理を制御するために使用されているわけではありません(つまり、正確ではありません)
    4.アイドルスレッドキューを再実現しようとすると、volatileとプログラムロジックに関連するhappen-beforeの関係をより多く追加する必要が避けられない.これにより、本来のプログラムロジックが曖昧になり、問題が発生する確率が非常に大きくなる.
    5.スレッドごとにinterruptを行うため、スレッドキューをもう1つ維持しなければならないため、
    ……
    当初の目標を達成するのがますます面倒になっていることに気づいた.
    これらの問題を持って、最終的にはThreadPoolExecutorの実現を見て、マスターがどのようにしたかを見に行きました.
    ThreadPoolExecutorの性能最適化はロックを減らす範囲でのみ行われ,ReentrantLockが用いられ,ほとんどのパラメータはvolatileが用いられていることが分かったが,可視性を保証するためにhappen-beforeルールを利用したコードはほとんど現れず,つまりThreadPoolExecutorの実現において最適化の程度はロックのレベルにすぎず,さらなる最適化は考慮されていない.クラスの著者を見るとDoug Leaです
    振り返ると、確かに、スレッドプールはAQSのフレームワークとは異なり、このスレッドプールの実現は安定性、多重性、柔軟性、安全性を重視しているからです.一般的に、スレッドプール呼び出し時間と真のタスクの実行時間は1桁ではないので、そのパフォーマンスの消費をあまり気にする必要はありません.
    そのため、前述の「運転速度がThreadPoolExecutorの1.5倍」という理由も出てきました.タスクが簡単すぎるため、タスク呼び出し時間の割合が大幅に向上し、現実生活ではこのような状況は少ないはずです.また、それぞれの機能の実現は性能を消耗しなければならない(特に同時を指す)ため、高性能を要求する関連の実現を書く時、できるだけ需要を実行し、機能の需要を減らしてこそ、より良い性能を得ることができると説明した.
    ここではThreadPoolExecutorの収穫をもう一度書きます
    1.次のリリースが拒否されたタスクを制御するためにRejectedExecutionHandlerを登録できます.
    2.threadFactoryを登録してスレッドを作成できます(必要なスレッドを自由にカスタマイズできます)
    3.学ぶべき方法があります.
    通常の論理では、一般的なシーンと例外シーン(一般的なシーンではロックは必要ありませんが、例外シーンでは必要です)があります.例外シーンはできるだけ少ない変数で入るかどうかを判断し、一般的なシーンは例外シーンと完全に分離する必要があります(再利用する方法があっても).
    例を挙げる
    
    public void execute(Runnable command) {
            if (command == null)
                throw new NullPointerException();
            if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
                if (runState == RUNNING && workQueue.offer(command)) {//    
                    if (runState != RUNNING || poolSize == 0)//          
                        ensureQueuedTaskHandled(command);//    ,     
                }
                else if (!addIfUnderMaximumPoolSize(command))
                    reject(command); // is shutdown or saturated
            }
        }
    

    4.クラスで定義された変数は、どのロックによって守られているか(つまり使用上、このロック状態で修正が許可されなければならない)を注釈で説明しているので、「java concurrency in practice」で同時関連annotationを使用することを提案したことを思い出します.
    後でネットでこのライブラリが提供されているかどうかを探して、コンパイル期間中にannotationに基づいてエラーをチェックできればもっといいです.
    5.BlockingQueueインタフェースの既存定義を用いて実現する:スレッドの切り替え消費を減らすために、各スレッドはタスクキューが空になった後、一定時間の待ち時間を行う.(明確に定義されていることは知られていないが、以前読んだいくつかの文章によれば、jvmの実装では、時間が短ければスレッドを切り替えるのではなく、ループメカニズムで実現されると推定される)
    6.スレッドの待ちと起動もBlockingQueueによって実現され、ThreadPoolExecutorはプールの状態だけを維持し、スレッドのいかなる状態情報も維持しない.スレッドの基本動作はBlockingQueueによって制御される.これは実際にはある程度、同時の複雑さを低減し、ThreadPoolExecutorの機能をより簡単にし、より理解しやすくする.拡張しやすくなりました
    7.最適化キーステップ:スレッドプールのように、最も重要なステップ(つまり呼び出される回数が最も多く、実装全体で最も消費される部分)はgetTask、execute、runTaskメソッドであり、他の方法では、呼び出される回数が少なすぎるため、shutdown(一度)、addThreadのように、効率を追求する必要はありません.エラーを避けるために、ロックをかけることを考えることができます.しかし、重要なステップについては、できるだけ最適化し、ロックを少なくしたり、ロックをしないようにしなければなりません.3つのテクニックのように
    8.ThreadPoolExecutorもスレッドに送信されたタスクを回収するのではなく、それを実行します(つまりgetTask動作とrunTask動作の間には、閉じたイベントをタイムリーに発見できるメカニズムはありません).
    総じて言えば、効率的で強力なスレッドプールを実現するための以前の計画は、まだ問題があります.
    JDKのThreadPoolExecutorでも、それはできません.各機能の実現には具体的なニーズがあるべきで、性能を優先するか他を優先するかは根拠があり、優先は一方では他方を犠牲にしなければならない.JDKのようなThreadPoolExecutorも多重化、安定化、フル機能を実現するため、ある程度の性能を犠牲にしています.