程序员技术交流

ThreadLocal实现与内存泄漏

2018-07-22  本文已影响48人  黄金矿工00七

JDK中对ThreadLocal的定义:

  1. 首先,ThreadLocal内部并没有维护一个ThreadLocalMap,这个map的引用是在每个线程中。每个线程都拥有一个ThreadLocalMap对象,他的key是ThreadLocal实例对象。value是对应的变量副本,也就是说ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
  2. set、get操作都是通过Thread.currentThread()来获得线程自己的ThreadLocalMap,根据ThreadLocal自身来查找对应的value(每一个ThreadLocal对象有一个创建时生成唯一的HashCode,即 nextHashCode(),通过取模确定所在槽下标位置)
  3. ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
  4. 每个线程独自拥有一个变量,并非是共享的。(!!!如果把一个共享的对象直接保存到ThreadLocal中,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 所以要在保存到ThreadLocal之前,通过克隆或者new来创建新的对象,为每个线程创建一个独立的变量副本。
ThreadLocalMap实现:

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

/**
  * 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<ThreadLocal<?>> {
    /** 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 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);
}
/**
  * 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 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
上一篇下一篇

猜你喜欢

热点阅读