ThreadLocal保姆级解析

75 阅读15分钟

ThreadLocal是什么?

官方解释为:ThreadLocal是用来提供线程内部的局部变量,这个变量在多线程的环境下访问(通过get和set方法访问)时能够保证各个线程变量相对独立与其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程的上下文。

总结一下:ThreadLocal就是提供线程内的局部变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

ThreadLocal基本使用

ThreadLocal常用方法

方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

ThreadLocal与synchronized的区别

synchroniezdThreadLocal
原理同步机制采用“时间换空间”的方式,只提供了一份变量,让不同的线程排队访问ThreadLocal采用“空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点多个线程之间访问资源的同步多个线程中让每个线程之间的数据相互隔离

使用ThreadLocal的好处

  • 传递数据:保存每个线程绑定的数据,在需要的地方可以直接使用,避免了参数直接传参造成的代码耦合。
  • 各线程之间的数据相互隔离却又具备并发性,避免同步的方式造成性能损失。

ThreadLocal底层实现

jdk1.8之前

image.png

jdk1.8及之后

image.png

ThreadLocal 1.7版本的时候,entry对象的key是Thread。

1.8版本entry的key是ThreadLocal。

1.8版本的好处 :

  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。因为ThreadLocalMap是在Thread里面的,所以只要Thread消失了,那ThreadLocalMap就不复存在了。
  • ThreadLocalMap中的Entry数量变少。

ThreadLocal核心源码分析

Set方法

    /**
    * 设置当前线程对饮的ThreadLocal的值
    * @param value: 将要保存的当前线程对应的值
    */
    public void set(T value) {
         //获取当前线程
        Thread t = Thread.currentThread();
        //获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //判断map是否存在
        if (map != null) {
            //存在则调用map,set设置此实体entry
            map.set(this, value);
        } else {
            // 1)当前线程Thread不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象初始化
            // 3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        }
    }
    
    /** 
    *获取当前线程Thread对应维护的ThreadLocalMap 
    *
    *@param t 当前线程
    *@return 对应线程维护的ThreadLocalMap
    */
    ThreadLocalMap getMap(Thread t){
        return t.threadLocals;
    }
    
    /** 
    *创建当前线程Thread对应维护的ThreadLocalMap 
    *
    *@param t 当前线程
    *@param firstValue 存放到map中第一个entry的值
    */
    void create(Thread t, T firstValue){
        //this是调用当前方法的threaLocal实例
        t.threadLocals = new ThreadLocalMap(this,firstValue);
    }

代码执行流程:

  1. 首先获取当前线程,在获取当前线程维护的ThreadLocalMap
  2. 如果获取的ThreadLocalMap对象为不为null,则将参数设置到Map中(当前的ThreadLocal的引用作为Key)
  3. 如果获取的ThreadLocalMap对象为null,则给该线程创建ThreadLocalMap,并且设置初值(惰性初始化)。

get方法

    /** 
    *返回当前线程中保存ThreadLocal的值
    *如果当前线程没有此ThreadLocal变量
    *则它会通过调用{@link #initialValue}方法进行初始化值
    *
    *@return 返回当前线程对应的ThreadLocal的值 
    */
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map存在
        if (map != null) {
            //以当前的ThreadLocal为key,调用getEntry获取对应的存储实体
            ThreadLocalMap.Entry e = map.getEntry(this);
            //对e进行判空
            if (e != null) {
                //告诉编译器忽略 unchecked 警告信息,如使用List,ArrayList等未进行参数化产生的警告信息
                @SuppressWarnings("unchecked")
                //获取存储实体e对应的value值
                //即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /* 
            初始化:有两种情况有执行当前代码
            第一种情况: map不存在,表示此线程没有维护ThreadLocalMap对象
            第二种情况: map存在,但是没有与当前ThreadLocal关联的entry
        */
        return setInitialValue();
    }
    
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
    /** 
    * 初始化
    *
    */
    private T setInitialValue() {
        //调用initialValue获取初始化的值
        //此方法可以被子类重写,如果重写默认返回 null
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //判断map是否存在
        if (map != null) {
            //存在则调用map.set方法设置此实体entry
            map.set(this, value);
        } else {
            // 1)当前线程Thread不存在ThreadLocalMap对象
            // 2)则调用createMap进行TreadLocalMap对象初始化
            // 3)并将t(当前线程)和Value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        //返回设置的值value
        return value;
    }

代码执行流程

  1. 首先获取当前线程,根据当前线程获取一个Map一个。首先获取当苟线程,根据当前线程获取一个地图

  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4

  3. 如果e不为null,则返回e.value,否则转到4

  4. Map为空或者e为空,则通过initialNalue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结: 先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。

remove方法

/**
* 删除当前线程中保存的ThreadLcoal对应的实体entry
*/
public void remove() {
    // 获取当前线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    //如果此map存在
    if (m != null) {
        //存在则调用map.remove
        // 以当前ThreadLocal为key删除对应的实体entry
        m.remove(this);
    }
}

代码执行流程

  1. 首先获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

inirialValue方法

    /* 
    * 会返回当前线程对应的ThreadLocal的初始值
    *
    * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
    * 除非线程先调用了set方法,在这种情况下,initialvalue 才不会被这个线程调用。
    * 通常情况下,每个线程最多调用一次这个方法。
    * <p>这个方法仅仅简单的返回nu11 {@code null};
    * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值
    * 必须通过子类继承{0code ThreadLoca1}的方式去重写此方法
    * 通常,可以通过匿名内部类的方式实现
    * @return 当前ThreadLocal的初始值
    */
    protected T initialValue() {
        return null;
    }

此方法的作用是返回该线程局部变量的初始值。 (1)这个方法是一个延迟调用的方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行一次。

(2)这个方法确省实现直接返回一个null。

(3)如果想要一个除null之外的初始值,可以重写此方法。(该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

ThreadLocalMap源码分析

image.png

ThreadLocalMap是TheadLocal的静态内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。

成员变量

    /**
    * 初始容量 -- 必须是2的整次幂。
    */
    private static final int INITIAL_CAPACITY = 16;
    
    /**
    * 存放数据的table
    * 同样,数组的长度必须是2的整数次幂。
    */
    private Entry[] table;
    
    /**
    * 数组里卖弄entrys的个数,可以用于判断table当前使用量是否超过阈值。
    */
    private int size = 0;
    
    /**
    * 进行扩容的阈值,表使用量大于它的时候进行扩容。
    */
    private int threshold; // Default to 0

存储结构-Entry

 /*
 * Entry继承weakRefer ence,并且用ThreadLocal作为key.
 * 如果key为null(entry.get() == nu11),意味着key不再被引用,
 * 因此这时候entry也可以从table中清除。
 */
 static class Entry extends WeakReference<ThreadLocal<?>> {
     object value;
     
     Entry(ThreadLocal<?> k , object v){
         super(k);
         value = v;
     }
 }
  • 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。

  • 另外,Entry继承WeakReference,也就是key ( ThreadLocal )是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

相关名词解释

(1) 内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • oMemory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

(2)弱引用相关概念 Java中的引用有4种类型︰强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:

  • 强引用(“Strong”"Reference ),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用( WeakReference ),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

如果key使用强引用

image.png

  1. 假设在业务代码中使用完ThreadLocal , threadLocal Ref被回收了。

  2. 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

  3. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链threadRef->currentThread->threadLocalMap->entry。Entry就不会被回收(Entry中包括了ThreadLocal实例和value ) 。导致Entry内存泄漏。

也就是说,ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。

如果key使用弱引用

image.png

  1. 同样假设在业务代码中使用完ThreadLocal , threadLocal Ref被回收了。
  2. 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
  3. 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry ->value , value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

造成内存泄漏的真实原因

比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。 那么内存泄漏的的真正原因是什么呢? 细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:

1.没有手动删除这个Entry

2.currentThread依然运行

第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

为什么使用弱引用

根据刚才的分析,我们知道了︰无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。 要避免内存泄漏有两种方式︰

1.使用完ThreadLocal,调用其remove方法删除对应的Entry

2使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为null的话,那么是会对value置为null的。

这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障∶弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用 set,get,remove中的任一方法的时候会被清除,从而避兔内存泄漏。

HASH冲突的解决

ThreadLocalMap的构造方法

/**
* firstKey: 本ThreadLocal实例(this)
* firstValue:要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化table
    table = new Entry[INITIAL_CAPACITY];
    //计算索引(重点代码)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //设置值
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //设置阈值
    setThreshold(INITIAL_CAPACITY);
}

构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中 ,并设置size和threshold。

重点代码分析

private final int threadLocalHashcode = nextHashcode();
private static int nextHashcode( {
return nextHashcode.getAndAdd(HASH_INCREMENT);
}
AtomicInteger是一个提供原子操作的Int eger类,通过线程安全的方式操作加减,适合高并发情况下的使用private static AtomicInteger nextHashcode = new AtomicInteger(O);
/特殊的hash值
private static final int HASH_INCREMENT = Ox61c88647;

a.这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT, HASH_INCREMENT =0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[]table中,这样做可以尽量避免hash冲突。

b.关于&(INITIAL_CAPACITY - 1) 计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的 一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证保证在索引不越界的前提下使得hash发生冲突的次数减小。

ThreadLocalMap的set方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    //计算索引
    int i = key.threadLocalHashCode & (len-1);
    /*
        使用线性探测法查找元素(重点代码)
    */
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        //ThreadLocal的key存在直接覆盖值
        if (k == key) {
            e.value = value;
            return;
        }
        // key为null,但是值不为null,说明之前的 ThreadLocal 对象已经被回收了,
        //当前数组中的Entry是一个陈旧( stale )的元素
        if (k == null) {
            //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry.
    tab[i] = new Entry(key, value);
    int sz = ++size;
    /*
        cleansomeslots用于清除那些e.get()==null的元素,这种数据key关联的对象已经被回
        收,所以Entry(table[index])可以被置未null.
        如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
        rehash(执行一次全表的扫描清理工作)
    */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
//获取环形数组的下一个索引
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

代码执行流程:

A.首先还是根据key计算出索引i,然后查找i位置上的Entry 。

B.若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。

C.若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry。

D.不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析: ThreadLocalMap使用线性探测法来解决哈希冲突的。

该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把Entry[]table看成一个环形数组。