ThreadLocal 以及内存泄漏导致的内存溢出问题

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。

值得一提的是,在JDK5.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<SimpleDateFormat> 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<SimpleDateFormat> 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();
            ;
        }
    }

这个例子中输出主线程的s3的toString()方法,工作线程输出s5的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<SimpleDateFormat> 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();
    }
    }

这种比较方式是合理的,只能采用 == 的方式去比较, == 是比较两个对象是否是同一个对象


使用ThreadLocal还有一种方法:

    class DateFormatUtils { 
    private static ThreadLocal<SimpleDateFormat> td = new ThreadLocal<SimpleDateFormat>(){
        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的子类里面重写父类的方法

上面这个例子,开启了两个线程,那么就会走两次initialValue()方法,创建两次SimpleDateFormat对象

打印结果:   
=================initialValue()=================
=================initialValue()=================

ThreadLocal的详细解释(具体例子)

案例一:

public class ThreadLocalTest {
public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>() {
    //重写父类的方法
    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没每一个线程单独保存一份副本:

打印结果:(五个线程打印的变量都是一致的)

    CounterThread-[0] : 100
    CounterThread-[1] : 100 
    CounterThread-[3] : 100
    CounterThread-[2] : 100
    CounterThread-[4] : 100     

案例二:(下面这个例子会出现问题)

        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<Index> local = new ThreadLocal<Index>() {
            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每一次是把一个引用当做副本保存在作为当前线程的值来保存,这样会出问题的,因为这个引用是会改变值的,只需要把return num; 换成 return new Index(),这样就不会有问题.

案例三(内存泄露与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回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。
只有当前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.

这里面有两个类.一个是MyThreadLocal继承了ThreadLocal,里面有一个成员变量(用来模拟占用比较的的内存),一个字节写的一个类,MyThreadLocal的key是当前的线程对象,value是My50MB这个类对象,main方法里面,将t1 = null,断开了ThreadLocal的强引用,然后强烈建议gc过来回收,MyThreadLocal会立即回收,但是My50MB这个类的对象并不会立即回收,有时需要等程序运行结束,线程结束之后,才会回收

解释:
每个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.

关于弱引用,老师也讲了这两个案例:

案例一:
    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();
    }
    }

这会造成内存泄漏问题,即使Outer01的对象设置为空了,并且强烈建议gc来回收,但是,由于如果内部类没有加上static修饰的内部类,
那么这个内部类是要依存于外部类(内部类没有停止的时候,外部类也不能被回收)
该进的方法:就是给外部类加上static修饰,

内部类加上了static修饰之后,那么这个内部类就不需要依赖外部类,即使内部类还在运行,外部类没有强引用,gc可以进来会说这个外部类.



案例二:

强引用:
    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这个对象,
他们直接是强引用的关系

Outer02 —> Inter02 –> TQueue –> Outer02 (存在着强引用)

打印结果:

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

Outer02这个对象并没有被回收,这就存在内存泄漏问题,因为Outer02这个类的对象已经置为空了,并且强烈建议gc来回收但是仍然没有,

即使Outer02的内部类Inner02这个类用static修饰,也没有用,以为Inteer02需要引用TQueue,而TQueue又需要Outer02,并且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<TQueue> weakR;

        public Inner02(TQueue tQueue) {
            this.weakR = new WeakReference<TQueue>(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回收的.

猜你喜欢

转载自blog.csdn.net/qq_38200548/article/details/80162972