ThreadLocalおよびメモリ漏洩によるメモリオーバーフローの問題

20225 ワード

ThreadLocal
1.ThreadLocalの理解
*1.1
      JDK 1.2       java.lang.ThreadLocal,ThreadLocal                      。                
        。
       ThreadLocal     ,ThreadLocal                    ,                    ,       
        。
           ,              ,      “Local”       。
*1.2
    ThreadLocal      ,  4   ,        :
    * void set(Object value)
                       。
    * public Object get()
                           。
    * public void remove()
                     ,            ,    JDK 5.0     。      ,      ,              
        ,                          ,             。
    * protected Object initialValue()
                     ,      protected   ,              。             ,
        1   get() 
    set(Object)    ,     1 。ThreadLocal            null。
特筆すべきは、JDK 5.0では、ThreadLocalが汎用型をサポートしており、クラス名がThreadLocalに変更されていることです.APIメソッドもそれに応じて調整され、新しいバージョンのAPIメソッドはvoid set(T value)、T get()およびT initialValue()である.
*1.3
    ThreadLocal                    ?
              : ThreadLocal     Map,              ,Map          ,           
2.ケーススタディ
需要:SimpleDateFormatはスレッドが安全ではないクラスですが、プロジェクトでは日付の変換にこのクラスが必要です.コンテナフォーマットの変換が必要なページがあり、複数人でアクセスする必要がある場合は、デフォルトで同じSimpleDateFormatを使用すると(コンテナ変換クラスに書いてstaticで修飾)、スレッドセキュリティの問題が発生しやすくなります.このSimpleDateFormatが共有されていない場合、アクセスするたびにオブジェクトが作成されると、オブジェクトが作成されすぎてメモリ消費量が大きくなります
この場合、比較的折衷する方法があります.これは、スレッドごとにSimpleDateFormatを個別に作成することです.これにより、同じスレッドの内部で同じSimpleDateFormatが使用されますが、異なるスレッドではSimpleDateFormatが使用されません.
     :

    class DateUtils {

        /*
         * SimpleDateFormet 1)      2)            (      )
         * 3)               (       )
         */

        /*
         * ThreadLocal           : 1)               2)             
         */
        private static ThreadLocal td = new ThreadLocal<>();

        //       
        public static SimpleDateFormat getInstance() {
            // 1.         
            SimpleDateFormat sdf = td.get();

            if (sdf == null) {
                sdf = new SimpleDateFormat("yyyy-MM-hh");
            }
            td.set(sdf);
            return sdf;
        }
    }

    public class ThreadLocalDemo2 {

        public static void main(String[] args) {
            SimpleDateFormat s1 = DateUtils.getInstance();
            SimpleDateFormat s2 = DateUtils.getInstance();
            SimpleDateFormat s3 = DateUtils.getInstance();

            System.out.println("main:" + (s1 == s2));
            System.out.println("main:" + (s2 == s3));
            new Thread(() -> {
                SimpleDateFormat s5 = DateUtils.getInstance();
                SimpleDateFormat s6 = DateUtils.getInstance();
                SimpleDateFormat s7 = DateUtils.getInstance();
                System.out.println(s5 == s6);
                System.out.println(s6 == s7);
            }).start();
            ;
        }
    }
印刷結果:
main:true
main:true
true
true
分析:
    DateUtils :    getInstance()  ,         ,  ThreadLocal   (static  )  get()  ,ThreadLocal      
    SimplateDateFormat,get             SimpleDateFormat  ,          ,      ,       ,    set  
    ,   ThreadLocal  ThreadLocalMap ,  Map   key         , ,               

    s1,s2,s3        ,  DateUtils.getInstance()  ,  SimpleDateFormat  ,      ,                 

    s5,s6,s7       ,ThreadLocal             SimpleDateFormat  ,                
注意:メインスレッドのSimpleDateFormatオブジェクトは、ワークスレッドのSimpleDateFormatオブジェクトとは異なります.検証の際、次のような検証方法が不合理であるという問題があります.
        class DateUtils {
        private static ThreadLocal td = new ThreadLocal<>();
        public static SimpleDateFormat getInstance() {
            SimpleDateFormat sdf = td.get();
            if (sdf == null) {
                sdf = new SimpleDateFormat("yyyy-MM-hh");
            }
            td.set(sdf);
            return sdf;
        }
    }
    public class ThreadLocalDemo2 {
        public static void main(String[] args) {
            SimpleDateFormat s1 = DateUtils.getInstance();
            SimpleDateFormat s2 = DateUtils.getInstance();
            SimpleDateFormat s3 = DateUtils.getInstance();
            //   s3
            System.out.println("s3" + s3);
            System.out.println("main:" + (s1 == s2));
            System.out.println("main:" + (s2 == s3));
            new Thread(() -> {
                SimpleDateFormat s5 = DateUtils.getInstance();
                SimpleDateFormat s6 = DateUtils.getInstance();
                SimpleDateFormat s7 = DateUtils.getInstance();
                //  s5
                System.out.println("s5:" + s5);
                System.out.println(s5 == s6);
                System.out.println(s6 == s7);
            }).start();
            ;
        }
    }
この例ではメインスレッドを出力するs 3のtoString()メソッドと、ワークスレッド出力s 5のtoString()メソッドとを比較し、結果:
    s3java.text.SimpleDateFormat@f67a0280
    main:true
    main:true
    s5:java.text.SimpleDateFormat@f67a0280
    true
    true
toSting()メソッドで比較しようとすると、できない、toString()はHashCodeを利用して文字列を生成していると思って、異なるオブジェクトでもHashCodeが同じである可能性があるので、toString()でオブジェクトが同じかどうかを比較するのは正しくない方法です.
正しい方法:
    class DateUtils {
    private static ThreadLocal td = new ThreadLocal<>();
    public static SimpleDateFormat getInstance() {
        SimpleDateFormat sdf = td.get();
        if (sdf == null) {
            sdf = new SimpleDateFormat("yyyy-MM-hh");
        }
        td.set(sdf);
        return sdf;
    }
    }
    public class ThreadLocalDemo2 {
    //static SimpleDateFormat s5;
    public static void main(String[] args) {
        SimpleDateFormat s1 = DateUtils.getInstance();
        SimpleDateFormat s2 = DateUtils.getInstance();
        SimpleDateFormat s3 = DateUtils.getInstance();
        System.out.println("main:" + (s1 == s2));
        System.out.println("main:" + (s2 == s3));
        new Thread(() -> {
            SimpleDateFormat s5 = DateUtils.getInstance();
            SimpleDateFormat s6 = DateUtils.getInstance();
            SimpleDateFormat s7 = DateUtils.getInstance();
            System.out.println(s3 == s5);
            System.out.println(s5 == s6);
            System.out.println(s6 == s7);
        }).start();
    }
    }
この比較方式は合理的で、==の方式で比較することしかできなくて、==は2つの対象が同じ対象かどうかを比較するのです
ThreadLocalを使用するには、次の方法があります.
    class DateFormatUtils { 
    private static ThreadLocal td = new ThreadLocal(){
        protected SimpleDateFormat initialValue() {
            System.out.println("=================initialValue()=================");
            return new SimpleDateFormat("yyyy-MM-hh");
        };
    };

    public static String convertDate(Date date) {
        return td.get().format(date);

    }
    }
    public class ThreadLocalDemo3 {
    public static void main(String[] args) {
          String dateStr1 = DateFormatUtils.convertDate(new Date());
          String dateStr2 = DateFormatUtils.convertDate(new Date());
          String dateStr3 = DateFormatUtils.convertDate(new Date());

         new Thread(()->{
              String dateStr4 = DateFormatUtils.convertDate(new Date());
              String dateStr5 = DateFormatUtils.convertDate(new Date());
              String dateStr6 = DateFormatUtils.convertDate(new Date());
          }).start(); 
    }
    }
これはThrealのinintalValue()メソッドで、DateFormatUtilsのconverDate()メソッドを呼び出す別のオブジェクトがあるたびにSimpleDateFormatオブジェクトが作成され、現在のスレッドのオブジェクトが作成されます.その後、このスレッドは作成されず、同じスレッドが使用されます.
注意initialValue()は匿名のThreadLocalの子クラスで親クラスを書き換える方法です
この例では、2つのスレッドを開くと、2回のinitialValue()メソッドを実行し、SimpleDateFormatオブジェクトを2回作成します.
    :   
=================initialValue()=================
=================initialValue()=================
ThreadLocalの詳細な説明(具体例)
ケース1:
public class ThreadLocalTest {
public static final ThreadLocal local = new ThreadLocal() {
    //       
    protected Integer initialValue() {
        return 0;
    };
};
//  
static class Counter implements Runnable {

    public void run() {
        //         ,    100 
        int num = local.get();
        for (int i = 0; i < 100; i++) {
            num++;
        }
        //            
        local.set(num);
        System.out.println(Thread.currentThread().getName() + " : " + local.get());
    }
}

public static void main(String[] args) {
    Thread[] threads = new Thread[5];
    for (int i = 0; i < 5; i++) {
        threads[i] = new Thread(new Counter(),"CounterThread-[" + i +"]");
        threads[i].start();
    }

}
}
この例では、ThreadLocalはスレッドごとにコピーを保存していません.
印刷結果:(5つのスレッドで印刷された変数はすべて一致しています)
    CounterThread-[0] : 100
    CounterThread-[1] : 100 
    CounterThread-[3] : 100
    CounterThread-[2] : 100
    CounterThread-[4] : 100     
ケース2:(次の例では問題が発生します)
        package threadLocal;

        public class ThreadLocalTest01 {
        static class Index {
            private int num;
            public void increase() {
                num++;
            }
            public int getValue() {
                return num;
            }
        }
        private static Index num = new Index();

        //    Index        
        public static final ThreadLocal local = new ThreadLocal() {
            protected Index initialValue() {
                System.out.println(num.getValue());
                return num;
            }
        };
        //  
        static class Counter implements Runnable {

            public void run() {
                Index num = local.get();
                for (int i = 1; i < 1000; i++) {
                    num.increase();
                }
                //            
                local.set(num);
                System.out.println(Thread.currentThread().getName() + " : " + local.get().getValue());
            }
        }

        public static void main(String[] args) {
            Thread[] threads = new Thread[5];
            for (int i = 0; i < 5; i++) {
                threads[i] = new Thread(new Counter(),"CounterThread-[" + i + "]");
            }
            for (int i = 0; i < 5; i++) {
                threads[i].start();
            }
        }
        }
    0
    0
    0
    0
    CounterThread-[3] : 2997
    CounterThread-[2] : 3996
    0
    CounterThread-[0] : 4995
    CounterThread-[4] : 1998
    CounterThread-[1] : 999 
このケースで問題が発生した原因:ThreadLocalのinitialValue()メソッドはnum(参照)を返します.これは問題があります.ThreadLocalが毎回1つの参照をコピーとして現在のスレッドの値として保存することに相当します.この参照は値を変更するので、return numだけが必要です.return new Index()に変更すれば問題ない.
ケース3(メモリ漏洩とWeakReference)
    public class ThreadLocalTest02 {    
    public static class MyThreadLocal extends ThreadLocal {
        private byte[] a = new byte[1024 * 1024 * 1];

        @Override
        public void finalize() {
            System.out.println("My threadlocal 1 MB finalized.");
        }
    }

    public static class My50MB {//         
        private byte[] a = new byte[1024 * 1024 * 50];

        @Override
        public void finalize() {
            System.out.println("My 50 MB finalized.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocal tl = new MyThreadLocal();
                tl.set(new My50MB());

                tl = null;//   ThreadLocal    
                System.out.println("Full GC 1");
                System.gc();

            }

        }).start();
        System.out.println("Full GC 2");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 3");
        System.gc();
        Thread.sleep(1000);
        System.out.println("Full GC 4");
        System.gc();
        Thread.sleep(1000);

    }
    }
    :
    Full GC 2
    Full GC 1
    My threadlocal 1 MB finalized.
    Full GC 3
    My 50 MB finalized.
    Full GC 4
解析:出力から、threadLocalの強い参照が切断されると、keyのメモリが解放されることがわかります.スレッドが終了するとvalueのメモリが解放されます.各threadにはmapが存在し、mapのタイプはThreadLocal.ThreadLocalMapである.Mapのkeyはthreadlocalインスタンスです.このMapは確かに弱い参照を使用していますが、弱い参照はkeyに対してのみ使用されています.各keyは弱い参照でthreadlocalを指します.threadlocalインスタンスをnullに設定すると、threadlocalインスタンスを指す強い参照はありませんので、threadlocalはgcによって回収されます.しかし、current threadから接続された強い参照があるため、valueは回収できません.現在のthreadが終了する以降のみ、current threadはスタックに存在する、強引用が切れ、Current Thread,Map,valueは全てGCで回収される.
ThrealLocalのメモリリークの問題:
ケースを見てみましょう.
        public class ThreadLocalTest02 {    
        public static class MyThreadLocal extends ThreadLocal {
            private byte[] a = new byte[1024 * 1024 * 1];

            @Override
            public void finalize() {
                System.out.println("My threadlocal 1 MB finalized.");
            }
        }

        public static class My50MB {//         
            private byte[] a = new byte[1024 * 1024 * 50];

            @Override
            public void finalize() {
                System.out.println("My 50 MB finalized.");
            }
        }

        public static void main(String[] args) throws InterruptedException {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    ThreadLocal tl = new MyThreadLocal();
                    tl.set(new My50MB());

                    tl = null;//   ThreadLocal    
                    System.out.println("Full GC 1");
                    System.gc();
                }

            }).start();
            System.out.println("Full GC 2");
            Thread.sleep(3000);
            System.gc();
            System.out.println("..........");
            System.out.println("..........");
            System.out.println("..........");
            System.out.println("..........");
            System.out.println("..........");
            System.out.println("..........");
        }
        }
印刷結果を見て分析します.
    Full GC 2
    Full GC 1
    My threadlocal 1 MB finalized.
    ..........
    ..........
    ..........
    ..........
    ..........
    ..........
    My 50 MB finalized.
この中には2つのクラスがあります.1つはMyThreadLocalがThreadLocalを継承しています.中にはメンバー変数(比較を占有するメモリをシミュレートするために使用されます)があり、1バイトで書かれたクラスがあります.MyThreadLocalのkeyは現在のスレッドオブジェクトで、valueはMy 50 MBというクラスオブジェクトです.mainメソッドでは、t 1=nullを切断し、ThreadLocalの強い参照を切断し、gcが回収することを強くお勧めします.MyThreadLocalはすぐに回収されますが、My 50 MBというクラスのオブジェクトはすぐに回収されず、プログラムの実行が完了するまでスレッドが終了しないと回収されない場合があります.
  :
  thread      map, map    ThreadLocal.ThreadLocalMap。Map  key   threadlocal  。  Map        ,     
    key。  key      threadlocal。  threadlocal    null  ,        threadlocal  ,  threadlocal   gc
  。  ,   value     ,       current thread        。
    thread    , current thread       ,     , Current Thread, Map, value    GC  .
                   gc  ,         。  value threadLocal  null              ,        
 “    ”。  ThreadLocal    ,       ,   remove()       ,   ThreadLocal       ,   OMM。
        :
To help deal with very large and long-lived usages, the hash table entries use  WeakReferences for keys.
弱引用について、先生もこの2つのケースを話しました.
   :
    class Outer01 {
    /**
     *                 ,            
     *               
     */


    class Inner01 extends Thread {
        public void run() {
            while(true) {

            }
        }
    }
    public Outer01() {
        new Inner01().start();
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize()");
    }

    }
    public class ThreadLocalDemo4 {
    public static void main(String[] args) {
        Outer01 o = new Outer01();
        o = null;
        System.gc();
    }
    }
これはメモリリークの問題となり、Outer 01のオブジェクトが空に設定されていてもgcで回収することを強く推奨するが、内部クラスにstatic修飾を加えた内部クラスがなければ、この内部クラスは外部クラス(内部クラスが停止していない場合、外部クラスは回収できない)に依存するため、外部クラスにstatic修飾を加えることで、
内部クラスにstatic修飾を加えると、この内部クラスは外部クラスに依存する必要がなく、内部クラスがまだ実行するも、外部クラスに強い参照がなく、gcが入ってきてこの外部クラスを言うことができる.
ケース2:
   :
    class TQueue {
    private Outer02 outer02;
    public TQueue(Outer02 outer02) {
        this.outer02 = outer02;
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("TQueue.finalize()");
    }
    }

    class Outer02 {
        public Outer02() {
            new Inner02(new TQueue(this)).start();;
        }
        static class Inner02 extends Thread {
            private TQueue tQueue;

            public Inner02(TQueue tQueue) {
                this.tQueue = tQueue;
            }

            public void run() {
                while(true) {
                    System.out.println(tQueue);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }


        @Override
        protected void finalize() throws Throwable {
            System.out.println("Outer02.finalize()");
        }

        }


        public class ThreadDemo5 {
        public static void main(String[] args) {
            Outer02 o = new Outer02();
            o=null;
            System.gc();
            //while(true){}
        }
        }
分析:
          ,new Outer02()         ,    Innerer02(       ),        ,          TQueue    ,
 TQueue      Outer02    ,
           
Outer 02->Inter 02->TQueue->Outer 02(強い参照が存在する)
印刷結果:
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
threadLocal.TQueue@3b1c2f38
Outer 02というオブジェクトは回収されていません.これは、Outer 02というクラスのオブジェクトが空になっているため、gcが回収することを強くお勧めしますが、まだありません.
Outer 02の内部クラスInner 02というクラスがstaticで修飾するも、Inteer 02はTQueueを引用する必要があり、TQueueはOuter 02を必要とし、staticは内部クラスにしか加算ことができず、外部クラスはstaticで修飾することはできないと考える.
この問題を解決する方法は弱い引用で解決しなければならない.
    package threadLocal;

    import java.lang.ref.WeakReference;

    class TQueue {
    private Outer02 outer;

    public TQueue(Outer02 outer) {
        this.outer = outer;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("TQueue.finalize()");
    }
    }

    class Outer02 {
    public Outer02() {
        new Inner02(new TQueue(this)).start();
    }

    static class Inner02 extends Thread {
        /*
         * private TQueue tQueue; public Inner02(TQueue tQueue){
         * this.tQueue=tQueue; }
         */
        //    
        private WeakReference weakR;

        public Inner02(TQueue tQueue) {
            this.weakR = new WeakReference(tQueue);
        }

        @Override
        public void run() {
            while (true) {
                //           
                System.out.println(this.weakR.get());
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                }
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize()");
    }
    }

    public class TestOOM02 {
    public static void main(String[] args) {
        //    
        Outer02 o2 = new Outer02();
        o2 = null;
        System.gc();
        // while(true){}

    }
    }
ここで面接は弱い引用があるので、gcで回収することができます.