深入理解ThreadLocal的原理及内存泄漏问题

        学习Java中常用的开源框架,Mybatis、Hibernate中设计到线程通过数据库连接对象Connection,对其数据进行操作,都会使用ThreadLocal类来保证Java多线程程序访问和数据库数据的一致性问题。就想深入了解一下ThreadLocal类是怎样确保线程安全的!详解如下:

        ThreadLocal的简单使用:

public class Test01 {
    ThreadLocal<Long> longLocal=new ThreadLocal<>();
    ThreadLocal<String> stringLocal=new ThreadLocal<>();

    public void set(){
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong(){
        return longLocal.get();
    }

    public String getString(){
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread().start();
        Test01 t=new Test01();
        t.set();
        System.gc();
        Thread.sleep(100);
        System.out.println(t.getLong());
        System.out.println(t.getString());


        Thread thread1 = new Thread(()->{
            t.set();
            System.out.println("\n子线程 Thread-0 :");

            System.gc();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(t.getLong());
            System.out.println(t.getString());
        });
        thread1.start();
    }
}
//输出结果
//1
//main
//
//子线程 Thread-0 :
//12
//Thread-1

   ThreadLocal主要为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。一个线程可以储存多个ThreadLocal的值,具体看上面的例子。

        关于ThreadLocal就不看源码了,简单的一批,关键是理解下面几点,就算真正了解了ThreadLocal:

        1.每个线程内部有一个Map,类型为ThreadLocal.ThreadLocalMap,(记住是每一个Thread都有一个)

        2.对于ThreadLocalMap中的每一个Entry来说,它的key是ThreadLocal,同时这个key为一个弱引用,当ThreadLocal没有强引用指向它的时候,下次垃圾回收必定将他回收,因为value是强引用,所以只要ThreadLocalMap存在不会被回收,又因为ThreadLocalMap是线程的成员变量,所以value会一直被保存到线程被销毁,这就有内存泄漏的问题(LZ当时想,为什么要设计成弱引用,还会出现内存泄漏问题,难道设计成强引用它不香吗?会不会设计JDK啊,不会的话就让我来,不过等后来彻底明白了原理,设计ThreadLocal得大神请收下我的膝盖),

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

 

        正如上图所画,当虚拟机栈中ThreadLocal的强引用ThreadLocalRef不再指向这个ThreadLocal了,他就会被回收。 

        3.ThreadLocalMap的key为ThreadLocal类型,value为任意对象,注意key是一个ThreadLocal类型,它是一个对象,而且多个线程内的ThreadLocal是共享的(注意这一点很重要,也是理解ThreadLocalMap内存泄漏和为什么使用软引用,ThreadLocal为什么都是定义成private static的关键

       对于不理解的同学,可以看一下上面的例子:

    ThreadLocal<Long> longLocal=new ThreadLocal<>();
    ThreadLocal<String> stringLocal=new ThreadLocal<>();

      对于这两个ThreadLocal,多个线程中的ThreadLocalMap的key都是longLocal和stringLocal对象的引用,可以使用longLocal和stringLocal来查找当前线程自己的值,他们使用的key都是longLocal和stringLocal对象的引用(再强调一下,这时思考如果有线程将longLocal和stringLocal置为了null,这时所有以longLocal和stringLocal对象的引用为key存储的值,还能被取出来吗,这个问题是内存泄漏的关键,如果你把这个问题整清楚了,就知道ThreadLocal为什么使用软引用和内存泄漏问题了

      

                       

        对于还有点迷糊的同学可以看一下上面这个图,每一个线程里面的ThreadLocalMap中的一个Entry中,key指向的是同一个ThreadLocal的对象引用,一个线程中可以存储有多个Entry,也就是多个ThreadLocal(就像例子里面的一样)。

       明白了上面三点,让我们来看一下下面几个问题:

       1.ThreadLocal中为什么ThreadLocalMap中的key是软引用?

         首先明白一点,在只有被软引用所指向的对象,下次GC时这个对象必定会被回收。

       我们假设key使用的是强引用,想要拿到存储在自身线程值的时候,需要以ThreadLocal作为key去取,也就是调用longLocal.get(),它会得到当前线程的ThreadLocalMap,以longLocal这个ThreadLocal对象本身作为key去查找相应的值,还是看一下ThreadLocal的get方法:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

        明白了这一点我们思考,如果此时将longLocal置为null,这样的话在所有线程内你就不能调用longLocal.get()获取值,这样就这个longLocal你就不能再使用了,因为你能使用到的引用已经没有指向这个ThreadLocal了,然后这个ThreadLocal对象也不会被回收( 我们已假设key使用的是强引用),因为在ThreadLocalMap中的key还指向这个ThreadLocal对象,然后这个时候还是会出现内存泄漏(惊不惊喜,意不意外!),最后总结出ThreadLocal使用强引用和软引用都会发生内存溢出的问题。

        那么为什么JDK的实现者会时候软引用呢?是这样的,软引用保证了在这个ThreadLocal对象在没有强引用的时候,会被回收掉,这个value怎么办呢?JDK实现者做了这样一个方法,在ThreadLocal对象的get,set和remove方法时,会去清除那些key为null的Entry,这样能够保持程序尽可能小的出现内存泄露问题(注意使用软引用能尽量小的保证出现内存泄漏问题),所以JDK选择使用软引用是最正确的选择,在使用ThreadLocal时,对于用不到的值,要尽量remove清除一下

      2.ThreadLocal的内存泄露问题?

      关于这个问题,其实明白ThreadLocal为啥用软引用后,这个问题就简单多了,在ThreadLocal对象没有强引用指向的时候,这个ThreadLocal对象对被回收,然而此时value并没有被回收就造成了内存泄漏,看图:

              

    3.为什么ThreadLocal对象最好使用peivate  static来修饰?

         这个阿里面试的时候问到过,就像下面这样使用

public class Test01 {
    private static ThreadLocal<Long> longLocal=new ThreadLocal<>();
    private static ThreadLocal<String> stringLocal=new ThreadLocal<>();

    public void set(){
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong(){
        return longLocal.get();
    }

    public String getString(){
        return stringLocal.get();
    }

}

       其原因如果上面看懂了,就很容易知道原因了,我就简单说一下,所有线程都会使用一个ThreadLocal对象作为key,若为类的实例对象(也就是不加static的),每一个实例对象的ThreadLocal都是不一样的。

发布了225 篇原创文章 · 获赞 30 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_35634181/article/details/103996977