ThreadLocal实现与内存泄漏

JDK中对ThreadLocal的定义:

  • ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题
  • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
  • 在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为他自身,而值对应线程的变量副本
    我们先来看一下他的实现原理:


    image.png
  1. 首先,ThreadLocal内部并没有维护一个ThreadLocalMap,这个map的引用是在每个线程中,ThreadLocal仅仅作为一个代理。每个线程都拥有一个ThreadLocalMap对象,他的key是ThreadLocal实例。value是对应的变量副本,所以对一个同一个ThreadLocal实例,每个线程的threadlocalmap的key都是一样的,因此每个线程都可以获得一份相同的变量副本。
  2. set、get操作都是通过Thread.currentThread()来获得线程自己的ThreadLocalMap,根据ThreadLocal自身来查找对应的value(每一个ThreadLocal对象有一个创建时生成唯一的HashCode,即 nextHashCode(),通过取模确定所在槽下标位置)
  3. ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
  4. 每个线程独自拥有一个变量,并非是共享的。(!!!如果把一个共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,为每个线程创建一个独立的变量副本。
  • ThreadLocal的作用:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,防止对可变的单例对象或者全局变量进行共享。作用:提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度(应用上下文),或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响(例如数据库连接)。
ThreadLocalMap实现:

ThreadLocalMap采用的是开地址法而不是链表来解决冲突。

  • ThreadLocalMap并没有使用链表或红黑树去解决hash冲突的问题,而仅仅只是使用了数组来维护整个哈希表
    • Entry的key设计成弱引用(Entry并非弱引用),因此key随时可能被GC(只能活到下次GC),尽量多的面对空槽(如果是链表或者红黑树那么弱引用会找不到对应的entry)
    • (单个ThreadLocal时)当遇到碰撞时,通过线性探测的开放地址法解决冲突问题
    • (多个ThreadLocal时)引入了神奇的0x61c88647,增强其的散列性,大大减少碰撞几率
/**
  * The entries in this hash map extend WeakReference, using
  * its main ref field as the key (which is always a
  * ThreadLocal object).  Note that null keys (i.e. entry.get()
  * == null) mean that the key is no longer referenced, so the
  * entry can be expunged from table.  Such entries are referred to
  * as "stale entries" in the code that follows.
  *     它使用主要的引用域作为自身的key(即ThreadLocal对象)
  *     由于Entry继承自WeakReference,而ThreadLocal被WeakReference封装
  *     !!重点:因此Entry的Key才是弱引用(而不是Entry)!!
  *     当调用get方法返回null时,这意味着该key不再被引用,因此该entry将会从数组中移除
  *     弱引用:当JVM在GC时如果发现弱引用就会立即回收
  *     Entry并没有使用HashMap.Entry的链表结构
  *   
  */
static class Entry extends WeakReference> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //当ThreadLocal的外部强引用被回收时,ThreadLocalMap的key会变成null
    //注意key是个ThreaLocal对象,但因为key被WeakReference封装,因此才具有弱引用特性
    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}
 /**
 * The initial capacity -- MUST be a power of two.
 *  容量必须2次幂,服务于Hash算法
 */
private static final int INITIAL_CAPACITY = 16;
/**
 * The table, resized as necessary. table.length MUST always be a power of two.
 *  底层实现还是一个Entry数组
 */
private Entry[] table;
/**
  * The number of entries in the table.
  * 数组已有元素数量
  */
private int size = 0;
/**
  * The next size value at which to resize.
  * 阈值,默认为0
  */
private int threshold; // Default to 0
/**
  * Construct a new map initially containing (firstKey, firstValue).
  * ThreadLocalMaps are constructed lazily, so we only create
  * one when we have at least one entry to put in it.
  *     默认构造器,包含一个键值对:一个ThreadLocal类型的key,一个任意类型的value
  *     createMap方法会直接使用该构造器一次性完成ThreadLocalMap的实例化和键值对的存储
  */
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //计算数组下标 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模运算优化版) 
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //在数组指定下标处填充数组
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);//默认阈值是 32/3 约等于 10.6667
}
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 *      取len的三分之二,而不是HashMap的0.75
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
  • set方法
/**
  * Set the value associated with key.
  *     存储键值对,Entry并不是链表,这意味着ThreadLocalMap底层只是数组
  *     其解决冲突(或者说散列优化)的关键在于神奇的0x61c88647
  *     若遇到过期槽,就占用该过期槽(会涉及位移和槽清除操作)
  *     当清理成功同时到达阈值,需要扩容
  * @param key the thread local object
  * @param value the value to be set
  */
private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;//数组容量
    //计算数组下标 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模运算优化版) 
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        //若key已存在,替换值即可
        if (k == key) {
            e.value = value;
            return;
        }
        //若当前槽为过期槽,就清除和占用该过期槽
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        //否则继续往后 直到找到key相等或第一个过期槽为止
    }
    //没有找到key相等或过期槽(key=null)则加入数组
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //当清理不成功同时到达阈值,需要扩容
    //cleanSomeSlots要处理的量是已有元素数量
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
/**
  * Increment i modulo len. 不超过长度就自增1
  */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}
  • getEntry方法
/**
  * Get the entry associated with key.  This method itself handles
  * only the fast path: a direct hit of existing key.
  * It otherwise relays to getEntryAfterMiss. This is
  * designed to maximize performance for direct hits, in part
  * by making this method readily inlinable.
  *     该方法自身只处理直接匹配到的情况,主要是最大化直接匹配的性能
  *     匹配不到的话就依赖getEntryAfterMiss方法
  * @param  key the thread local object
  * @return the entry associated with key, or null if no such
  */
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
        //找不到就依赖getEntryAfterMiss方法
        return getEntryAfterMiss(key, i, e);
}


/**
  * Version of getEntry method for use when key 
  * is not found in its direct hash slot.
  *     该方法用于根据下标不能直接找到的情况
  */
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    //不能直接匹配到的话,就只能循环遍历
    while (e != null) {
        ThreadLocal k = e.get();
        //找到立即返回
        if (k == key)
            return e;
        //遇到过期槽,移除过期槽
        if (k == null)
            expungeStaleEntry(i);
        else
        //否则继续往后遍历
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
  • remove方法
/**
  * Remove the entry for key.
  *     当找到该元素的时候,主要做了两个清洗操作
  *         1.将key(ThreadLocal)设置为null
  *         2.当前槽变成过期槽,因此要清除当前槽所存储的Entry元素(主要是避免内存泄露)
  */
private void remove(ThreadLocal key) {
    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)]) {
        if (e.get() == key) {
            e.clear();//会将key设为null -> this.referent = null
            expungeStaleEntry(i);//清除过期元素
            return;
        }
    }
}

因为每个线程都有一个ThreadLocalMap,他保存了变量的副本,所以这是线程隔离的根本

ThreadLocal内存泄漏
image.png
  • 原理分析:
    ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
    ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value,如果分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
  • 最佳实践:
    • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
    • 在使用线程池的情况下,当前线程不一定会退出,若没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

你可能感兴趣的:(ThreadLocal实现与内存泄漏)