ThreadLocal and memory overflow problems caused by memory leaks

ThreadLocal

1. Understand 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。

It is worth mentioning that in JDK5.0, ThreadLocal already supports generics, and the class name of this class has been changed to ThreadLocal. The API methods have also been adjusted accordingly. The API methods of the new version are void set(T value), T get() and T initialValue().


*1.3
    ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
    其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本

2. Case realization

Requirement: SimpleDateFormat is a thread-unsafe class, but we need this class for date conversion in the project. If there is a page that needs to be converted into container format, and multiple
people visit together, the same SimpleDateFormat is used by default. (Written in the container conversion class, decorated with static), thread safety problems will easily occur. If each, if the SimpleDateFormat is not shared, if an object is created every time it is accessed, too many objects will be created and memory consumption will be consumed. bigger

At this time, there is a compromise method, which is to create a separate SimpleDateFormat for each thread, so that the same SimpleDateFormat is used within the same thread, but different SimpleDateFormats are used in different threads.

看代码实现:

    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();
            ;
        }
    }

Printed result:

main:true
main:true
true
true

analyze:

    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对象,所以这三个变量指向的是同一个对象

Note: The SimpleDateFormat object in the main thread is different from the SimpleDateFormat object in the worker thread. When verifying, there is a problem.
The following verification method is an unreasonable verification method:

        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();
            ;
        }
    }

In this example, the toString() method of s3 of the main thread is output, and the toString() method of s5 is output by the worker thread, and the results are compared:

    s3java.text.SimpleDateFormat@f67a0280
    main:true
    main:true
    s5:java.text.SimpleDateFormat@f67a0280
    true
    true

It is not possible to use the toSting() method for comparison, because toString() is to use HashCode to generate strings, even different objects may have the same HashCode, so use toString() to compare whether the objects are the same is incorrect practice;


Correct way:

    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();
    }
    }

This comparison method is reasonable, and can only be compared by ==, which is to compare whether two objects are the same object


There is another way using 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(); 
    }
    }

This is using Threal's initialValue() method. Every time another object wants to call the convertDate() method of DateFormatUtils, a SimpleDateFormat object will be created, an object will be created for the current thread, and then it will not be created for this thread. both use the same,

Note that initialValue() is a method of overriding the parent class in the anonymous ThreadLocal subclass

In the above example, two threads are opened, then the initialValue() method will be called twice, and the SimpleDateFormat object will be created twice

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

Detailed explanation of ThreadLocal (specific example)

Case number one:

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();
    }

}
}

This example shows that ThreadLocal does not keep a separate copy for each thread:

Printing results: (The variables printed by the five threads are consistent)

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

Case 2: (The following example will have problems)

        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 

The reason for the problem in this case: The initialValue() method of ThreadLocal returns num (reference), which is problematic. It is equivalent to ThreadLocal saving a reference as a copy every time as the value of the current thread. There will be a problem, because this reference will change the value, just replace return num; with return new Index(), so there will be no problem.

Case 3 (memory leak and 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

Analysis: As can be seen from the output, once the strong reference of threadLocal is disconnected, the memory of the key can be released. Only when the thread ends, the memory of value is released.
There is a map in each thread, and the type of map is ThreadLocal.ThreadLocalMap. The key in the Map is a threadlocal instance. This Map does use weak references, but weak references are only for keys. Each key has a weak reference to threadlocal. When the threadlocal instance is set to null, there is no strong reference to the threadlocal instance, so the threadlocal will be reclaimed by gc. However, our value cannot be recycled because there is a strong reference connected from the current thread.
Only after the current thread ends, the current thread will not exist in the stack, the strong reference will be disconnected, and the Current Thread, Map, and value will all be recycled by the GC.

Memory leak problem with ThrealLocal:

Take a look at a case:

        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("..........");
        }
        }

Take a look at the printout for analysis:

    Full GC 2
    Full GC 1
    My threadlocal 1 MB finalized.
    ..........
    ..........
    ..........
    ..........
    ..........
    ..........
    My 50 MB finalized.

There are two classes in it. One is that MyThreadLocal inherits ThreadLocal, which has a member variable (used to simulate the memory occupied by comparison), a class written by one byte, the key of MyThreadLocal is the current thread object, and the value is My50MB In this class object, in the main method, t1 = null, which disconnects the strong reference of ThreadLocal, and then strongly recommends gc to come and recycle, MyThreadLocal will be recycled immediately, but the object of the class My50MB will not be recycled immediately, and sometimes it is necessary to wait for the program to run End, after the thread ends, it will be recycled

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

Regarding weak references, the teacher also talked about these two cases:

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

This will cause a memory leak problem, even if the object of Outer01 is set to be empty, and it is strongly recommended to recycle by gc, however, because if the inner class does not have a static-modified inner class,
then this inner class is dependent on the outer class (inner class). When the class is not stopped, the outer class cannot be recycled)
The method to enter is to add static modification to the outer class,

After the inner class is decorated with static, then the inner class does not need to depend on the outer class. Even if the inner class is still running and the outer class has no strong reference, gc can come in and say this outer class.



Case 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){}
        }
        }

analyze:

这里面是存在强引用的,new Outer02()创建这个对象的时候,需要创建Innerer02(线程类的的对象),并且启用这个线程,这个线程类有需要创建TQueue这个对象,
而TQueue对象由得依赖Outer02这个对象,
他们直接是强引用的关系

Outer02 —> Inter02 –> TQueue –> Outer02 (there is a strong reference)

print result:

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

The object Outer02 has not been recycled, which has a memory leak problem, because the object of the Outer02 class has been set to empty, and it is strongly recommended that gc be recycled but still not,

Even if the inner class Inner02 of Outer02 is modified with static, it is useless, thinking that Inteer02 needs to refer to TQueue, and TQueue needs Outer02, and static can only be added to inner classes, and outer classes cannot be statically modified.

The solution to this problem is to use weak references.

    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){}

    }
    }

There are weak references in this interview, so they can be recycled by gc.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325762779&siteId=291194637