JAVA同時プログラミング(五)——性能最適化(上)

10706 ワード

プログラムにロックを加えるのは非常に性能を消費し、ロックの申請と解放には大量の資源を消費する必要があるので、ロックされたプログラムを最適化し、プログラムが正しく動作することを保証する前提で性能を高める必要があります.
  • 私たちができるロックの占有時間を減らすロックの粒度を分離ロックで独占ロックの代わりに粗化
  • JVMでできるロック偏向軽量ロックスピンロック消去
  • ThreadLocal

  • 私たちにできることは
    ロックの占有時間を短縮
    次の2つの例を比較してみましょう.
    public synchronized void f(){
        method1();
        syncmethos();//    
        method1();
    }
    public void g(){
        method1();
        synchronized(this){
        syncmethos();//    
        }
        method1();
    }

    メソッドfでは、ロックリソースを解放するには、すべてのメソッドが完了した後にロックを取得する必要がある.しかし,方法gではsynchronizedコードブロックを用い,同期のみが必要な場所で鍵をかけ,他の方法では鍵をかける必要はない.これにより、スレッドがロックを占有する時間が短縮され、プログラムの同時性能が大幅に向上します.
    ロックの粒度を小さくする
    減小ロックの粒度の典型的な使用例は高性能の同期容器ConcurrentHashMapであり、なぜCollectionsを使用するよりも性能が優れているのか.synchronizedMapはいいですか?ロックの粒度を小さくしたからだ.Collections.synchronizedMapでは同期の問題を解決するためにロックが使用されていますが、ConcurrentHashMapにはロックがたくさんあり、異なるリソースには異なるロックがあり、スレッドが同じリソースにアクセスした場合、キューに並んでロックを取る必要があります.通俗的に言えば、ConcurrentHashMapはビルのように多くの異なる入り口があり、人々が異なる入り口を選ぶと、ブロックされません.Collections.synchronizedMapは入り口が1つしかないように、全員が並んで入ります.デフォルトでは、ConcurrentHashMapには16個のロックがあり、幸いにも16個のスレッドが異なるリソースにアクセスすれば、その性能は非同期コンテナHashMapとほとんど変わりません.
    排他ロックの代わりに分離ロックを使用
    分離ロックは分離された2つのロックです.似たような例は、前に述べた**読み書きロック**ReadWriteLockです.私たちが読み取るときに読み取りロックをかけ、書くときに書き込みロックをかけると仮定します.この2つのロックは分離されています.では、読み取りと書き込みを別々に実行するとブロックされず、読み取り-読み取りまたは書き込み-書き込みだけがブロックされます.ラインは独占ロック(すべての操作でブロックが発生する)に比べて性能が向上したが、ReadWriteLockは、分離所の拡張であり、リードロックを外し、書き込みだけがブロックされ、単純な分離ロックよりも性能が優れている.
    ロック粗化
    ロックの占有時間を減らすことで性能が向上するとはいえ、何事にも1度はある.次の例を示します.
    for(int i=0;i<100;i++)
        synchronized(this){
            //    
        }

    ここではsynchronizedブロックを用い,ロックの保持時間を短縮したが,同じロックに対して100回のリクエスト,同期,解放を行い,大量のシステムリソースを消費した.従って、上記の例では、ロックを太くすることがより合理的である.このように:
    synchronized(this){
    for(int i=0;i<100;i++)
            //    
        }

    占有時間は増加しましたが、ロックを1回のリクエスト、同期、および解放するだけです.
    JVMでできること
    ロックバイアス
    ロック偏向は、ロック操作に対する最適化手段である.スレッドがロックを取得すると、JVMはこのロックを偏向モードにし、このロックが再要求されると同期操作を行う必要がなくなり、ロックを申請する時間と消費量が減少し、システムのパフォーマンスが向上します.JAVA仮想マシンパラメータ-XX:+UseBiaseLockingを使用して、偏向ロックをオンにすることができます.注意:ロック競合が少ない場合、バイアスロックは比較的良好な最適化効果があるが、競合が激しい場合、このモードは失効する.
    軽量ロック
    軽量ロックの本質は、ロックの代わりにCAS操作を用いて同期を実現することであり、従来の使用ロック(反発)は重量ロックである.実現原理:仮想マシンのオブジェクトヘッダは2つの部分に分けられ、一部はオブジェクト自身の運転データ、例えばオブジェクトHashCode、オブジェクト世代年齢、公式には「Mark Word」と呼ばれ、もう一部はデータ型のポインタを格納するために用いられる.Mark Wordにはロックフラグビットが存在し、01はロックされていないか偏向可能であり、00は軽量レベルを表すロック、10は膨張を表す.コードが同期ブロックに入ると、オブジェクトがロックされていない(01)場合、現在のスレッドスタックフレームにMark Wordのコピーを格納するためのロック記録空間(record)が作成される.その後、JVMはCAS操作を使用してオブジェクトのMark Wordをrecordに向け、操作が成功するとオブジェクトのロックが取得され、ロックフラグビットは(00)となり、オブジェクトが軽量レベルのロックであることを示す.失敗した場合、JVMはまずMark Wordが現在のスレッドスタックフレームを指しているかどうかをチェックし、現在のオブジェクトがロックされている場合は、同期ブロックに直接アクセスして実行します.そうしないと、他のスレッドがロックをプリエンプトしていることを示します.複数のスレッドが1つのロックを競合する場合、ロックフラグは(10)であり、CAS動作が多くなるため、このようなロックが競合する環境では重量ロックよりも効率が低い.
    スピンロック
    スレッドのサスペンションはシステムのオーバーヘッドを増大させるので、スレッドが本当にサスペンションされないようにJVMは最後の努力--スピンロックをします.JVMの努力はこうです.スレッドがロックを取得できない場合、JVMは賭けをします.近い将来、スレッドはこのロックを取得するので、JVMはスレッドに何度も空のループをさせます.ロックを取得すると、スレッドは臨界領域に入り、失敗すると、本当に掛けられます.スレッドが空のコードを実行することによって生じるCPUの消費がスレッドのストラップバンドのシステムの追加オーバーヘッドより小さい場合、スピンロックは本当の意味を持つ.スピンロックが盲目的に空のループを実行すると、CPUのリソースが無駄に消費されるため、スレッドのスピン回数はデフォルトで10になります.
    ロック解除
    JVMは、JITコンパイル時にコンテキストをスキャンし、リソース共有が存在しないロックを除去し、意味のない要求ロックの時間を節約します.
    public void function(){
        ConcurrentHashMap map = new ConcurrentHashMap();
        //     
        //.......
    }

    コードではConcurrentHashMapを使用する必要はありません.map変数は単純なローカル変数であり、ローカル変数はオンラインスタックに割り当てられており、スレッドプライベート(ThreadLocal変数に似ていますが、すぐに説明します)であり、他のスレッドにアクセスすることはできませんので、ロックを追加する必要はありません.JVMがこれらの不要なロックを検出すると、自動的にクリアされます.
    ThreadLocal
    制御リソースの意外性に加えて、リソースを追加することで、すべてのオブジェクトのスレッドのセキュリティを保証することができます.例えば100人に1枚の表に記入させますが、1本のペンしかないので、みんなは列に並ぶしかありませんが、どのようにペンの数を増やして、人手に1本のペンを持たせると、すぐに任務を完成します.
    public class ThreadLocalDemo {
    
        public static class Pen {
            private int id;
    
            public Pen(int id) {
                this.id = id;
            }
    
            public String toString() {
                return " :   :" + id;
            }
        }
    
        public static class Student implements Runnable {
            private int sid;
            ThreadLocal local;
    
            Student(int sid, ThreadLocal local) {
                this.sid = sid;
                this.local = local;
            }
    
            @Override
            public void run() {
                // threadlocal  pen
                if (local.get() == null) {
                    //         ,     
                    local.set(new Pen(sid));
                }
                System.out.println("  :"+sid+"   "+local.get());
            }
        }
        public static void main(String[] args) {
            ThreadLocal local = new ThreadLocal<>();
            //     
            ExecutorService service = Executors.newCachedThreadPool();
            for(int i=1;i<=100;i++){
                service.execute(new Student(i, local));
            }
            service.shutdown();
        }
    }
    /*
    output:
      :5    :   :5
    。。。。。。
      :89    :   :55
      :88    :   :88
    。。。。。。
      :75    :   :75
    */
    

    私たちが注目しなければならないのはThreadLocalのsetとgetメソッドです.ソースコードのsetメソッドは次のとおりです.
     public void set(T value) {
             //         
            Thread t = Thread.currentThread();
            //            ThreadLocalMap
            //    ,          HashMap
            ThreadLocalMap map = getMap(t);
            //  map   ,  value  
            if (map != null)
                map.set(this, value);
            else
                //  map  ,     map  value  
                createMap(t, value);
        }

    getメソッドは次のとおりです.
     public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null)
                    return (T)e.value;
            }
            return setInitialValue();
        }

    getメソッドは,まず,現在のスレッドオブジェクトtを取得し,その後,自身をkeyとして内部の実際のデータを取得する.このように,上記のペン取り例では,実際に100個のpenのインスタンスが作成されていることが分かった.スレッドの実行が完了したら、このThreadLocalMapをクリーンアップするなど、これらのリソースをクリーンアップする必要があります.上記の例では、作成した100本のペンを破棄していないため、非常に大きな問題があります.さらに深刻なのは、スレッドプールExecutorsを使用しています.新CachedThreadPool()は、システムにメモリの漏洩が発生します.(100本のペンではクリーンアップできません).したがって、ThreadLocalを使用する場合は、ThreadLocal.remove()を使用して変数オブジェクトをクリーンアップすることが望ましいです.
    とても重要な点です!!!ThreadLocalは、スレッドごとにスレッドプライベートオブジェクトを作成するため、オブジェクトが異なるため、いわゆるスレッドセキュリティの問題はありません.ThreadLocalには100本のペンがありますが、形は同じですが、違う対象です.したがって、ThreadLocalは共有リソースへのスレッドセキュリティアクセスを実現することはできません.