ThreadLocal原理及内存泄漏分析

多线程访问同一个共享变量的时候回有并发问题,解决办法之一就是每个线程都访问本线程自己的变量来避免线程不安全问题。这个线程本地变量就是TreadLocal来实现的。

    • 引用和对象的关系???
    • ThreadLocal的实现原理
    • 内存泄露
    • 避免内存泄露最好的做法

引用和对象的关系???

在开始分析ThreadLocal之前,我们先回顾一下Java中的引用,只有理解了引用,才可以深入理解TheadLocal的实现原理。

下面这段代码中的第一行,obj就是对象的引用,而new Object() 将会在堆内存中产生了一个对象,obj这个引用指向了堆内存的实际对象,之后我们便可以在程序中通过这个引用来操作对象了

 Object obj  = new Object();
 obj = null;

上面第二行,当给obj赋值null时,只是表示obj这个引用不再指向堆中的object的对象,但这时对象实例依旧存在于堆中,除非被GC回收,那么在Java对象里,引用和对象有哪些关系呢?

自JDK1.2开始,Java提供了4中引用关系

  • 强引用(Strong Reference)
    ( “ 一直存活着 ”;
    类似 “Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的实例对象)
  • 软引用(Soft Reference)
    ( “ 有一次活的机会 ”;
    软引用关联着的对象在系统将要发生内存溢出异常之前,JVM垃圾收集器将会把这些对象实例进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常)
  • 弱引用(Weak Reference)
    ( “回收就会死亡 ”;
    被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的实例对象)
  • 虚引用(Phantom Reference)
    ( “ 随时可能被回收 ”
    也成为了幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象实例,为一个对象设置虚引用的唯一目的,就是能在这个实例对象被垃圾回收时,收到一个系统通知)
    这4种引用关系、强度由强到弱

ThreadLocal的实现原理

了解了Java的4中引用关系,接下来我们聊一聊ThreadLocal的实现原理
当使用TreadLocal维护变量的时候,该变量存储在线程本地,其他线程无法访问,做到了线程间隔离,也就没有线程安全问题了

接下来,我们来对TreadLocal进行详细分析下,首先,我们先来看ThreadLocal中数据是怎么存储的,以及一些引用关系

ThreadLocal原理及内存泄漏分析_第1张图片

每一个Thread中,都会有一个ThreadLocalMap对象
ThreadLocalMap中有一个Entry数组
一个Entry对象中又包含一个key一个value,key就是ThreadLocal的对象实例,注意,这里对ThreadLocal对象的引用是弱引用,value就是通过java.lang.ThreadLocal#set方法实际写入的值,程序允许时,栈中会存储对Thread和ThreadLocal的引用

接下来分析一下ThreadLocal的源码

public class Thread implements Runable{

  ThreadLocal.ThreadLocalMap threadLocals = null;
  
}

前面讲到ThreadLocalMap实际存储的在Thread对象中,每个Thread对象都有自己独有的ThreadLocalMap实例

我们再来看一下ThreadLocalMap中的重要属性

public class ThreadLocal {
    // 1
    static class ThreadLocalMap {

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

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

        private static final int INITIAL_CAPACITY = 16;                   //2


        private Entry[] table;                                         	  //3

        private int size = 0;               							  //4

        private int threshold;               							  //5

        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
    }
    
}
  • 注释1的地方,ThreadLocalMap中存储的值,实际就存储在Entry对象中,Entry是一个内部静态类,它继承了WeakReference>,Entry对象的key是ThreadLocal对象的弱引用,value是实际存储的值
  • 注释2-5,INITIAL_CAPACITY是初始化容量,它必须是2的整数次幂;
    table,一个Map中可以保存多少个ThreadLocal对象,这些对象存储在table数组中
    size,ThreadLcoalMap中总共存储了多少个ThreadLcoal对象
    threshold,下次扩容时应该扩容到多大

讲到这里,ThreadLcoal数据存储结构的源码就分析完了,接下来,我将通过分析set方法的源码带你来看一下数据存储到ThreadLcoal的过程

public void set(T value) {
        Thread t = Thread.currentThread(); 				 //1
        ThreadLocalMap map = getMap(t); 				 //2
        if (map != null){
           map.set(this, value); 				         //3
        } else{
            createMap(t, value); 				         //4
         }
    }
    
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; 				   //a
}

void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue); 			//b
}

上面代码中

  • 注释1,通过Thread。currentThread()来获取当前线程
  • 注释2,获取当前线程的ThreadLocalMap对象,getMap方法是返回t.threadLocals,这个threadLocals是Thread对象中的属性
  • 注释3,如果ThreadLocalMap不为空,则将对象值value设置到ThreadLocalMap 中,注意这里的this指的是ThreadLcoal对象
  • 注释4,如果ThreadLocalMap为空,则说明当前线程是首次设置ThreadLcoal的值,需要以当前ThreadLcoal对象作为key来创建ThreadLocalMap,并且将threadLocals这个引用指向新闯将的ThreadLocalMap对象

内存泄露

threadLocal的原理就介绍完了,接下来,就是我们在工作中使用threadLocal最常见的问题,即“内存泄露”,同事,也是面试中面试官经常问到的问题,现在我们就来分析一下内存泄露的原因及解决办法。需要强调的是,ThreadLocal本身不存储值,它只是作为一个key,来让线程从ThreadLocalMap中获取value,ThreadLocalMap是使用ThreadLcoal的弱引用作为key的。
ThreadLocal原理及内存泄漏分析_第2张图片一个对象如果只剩下弱引用,则该对象在GC时就会被回收,ThreadLocalMap使用ThreadLocal的弱引用作为key时,如果一个ThreadLocal没有外部强引用来引用它,比如手动将ThreadLocal A这个对象赋值为null,那么系统GC的时候,这个ThreadLocal A势必会被回收,这样一来ThreadLocalMap中就会出现key为null的Entry,Java程序没有办法访问这些key为null的Entry的value,如果当线程迟迟不结束的话,比如使用了线程池,或者线程还在执行其他耗时任务,那么这些可以为null的Entry的value就会一直存在一条强引用链。
Thread Ref引用Thead,Thead又引用ThreadLocalMap,ThreadLocalMap又引用了Entry,Entry对象又引用了value,这个map的key已经是null,这个value则永远无法回收,但是由于上述强引用链的存在,造成了内存泄露,只有当前thread结束以后,Thread Ref就不存在于栈中,强引用断开Thread对象,ThreadLocalMap对象,Entry数组,Entry对象,还有实际使用到的Object C对象,都将全部被GC回收

避免内存泄露最好的做法

  • 主动调用ThreadLocal对象的remove方法,将ThreadLocal对象中的值删除,实际上ThreadLocalMap设计中已经考虑了这种情况,也加上了一些防护措施,在调用ThreadLocal的get()、set()方法操作数据的时候,会清除当前线程的ThreadLocalMap里可以为null的value。如果ThreadLocal对象的强引用被删除后,线程长时间存货,有没有再对该线程的ThreadLocal对象进行操作,也就是没有调用get()、set()方法,那么依然会存在内存泄露。所以,使用ThreadLocal的时候,务必主动调用ThreadLocal对象的remove方法,将设置线程本地变量的

你可能感兴趣的:(ThreadLocal原理及内存泄漏分析)