重新认识 ThreadLocal 以及内存泄漏问题

来源:互联网 发布:mac pro玩dota2帧数 编辑:程序博客网 时间:2024/06/07 14:37

大部分人都接触过ThreadLocal并且都会用过,如果不去深究,会自然而然的理解为:ThreadLocal 就是把很多线程的各自值存在一个Map 里面,Key 是 线程,Value 是需要保存的 值,各自互不影响,当需要使用时,在以本身作为Key,取出对应的Value.  = > 从而会得出 ThreadLocal的目的是为了解决多线程访问资源时的共享问题. 这么理解就错了


接下来就不举例证明各个线程相互不影响(确实是相互不影响),直接从使用的角度、按照一般使用的顺序并结合源码开始解析ThreadLocal(从定义、设置默认值、设置Value值、获取value值、移除Value值)

1、首先我们定义一个对象,并重写了initialValue()方法来设置ThreadLocal的初始值:

    private static ThreadLocal<String> serviceNumberCache   = new ThreadLocal<String>() {                                                                @Override                                                                protected String initialValue() {                                                                    return "0000";                                                                };                                                            };

2、设置Value值

 serviceNumberCache.set("10001");

代码比较简单,话不多说,直接上对应源码:

 public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);    }

getMap() 源码:

  ThreadLocalMap getMap(Thread t) {   //这里是获取Thread里面的变量    return t.threadLocals;    }
threadLocals对象定义:

  /* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */     //这个是在Thread类里面 ThreadLocal.ThreadLocalMap threadLocals = null;

createMap()方法:

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

在赋值时,首先获取当前线程 t,然后调用getMap()方法,获取ThreadLocalMap,如果存在就赋值,不存在就创建一个新的ThreadLocalMap.

注意了:getMap() 方法返回的是 当前线程的一个属性(t.threadLocals),是存放的每个线程自己的东西,和其他线程不相干,这就很好的解释了ThreadLocal 和多线程共享一点关系都没有,ThreadLocal是各自线程取自己里面的东西,根本不存在共享问题,就是自己私有的。我们再看一下createMap()方法进一步验证,创建时也是将新增的 ThreadLocalMap 赋值给线程自己的对象 threadLocals,说明每一个Thread维护一个自己的ThreadLocalMap映射表,new ThreadLocalMap(this,firstValue),这里说明了 我们定义的ThreadLocat 是作为映射表里面的key,需要存储的值作为value.这样也证明了我们原先的理解(存放在一个Map 里面,Key 是 线程,Value 是需要保存的 值) 确实是错误的,这里并不存在一个共享变量.

总结一下ThreadLocal的设计思路:

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

这样设计的主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

3、获取Value值

serviceNumberCache.get();
get()对应的源码:

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

setInitialValue()对应的源码:

private T setInitialValue() {        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);        return value;    }

首先获取对应的线程,再调用getMap()方法,获取每个线程自己维护的ThreadLocalMap映射表,如果存在,就以定义的threadLocal作为Key取保存的对应的值,如果不存在,就取默认值.

以上是结合源码对ThreadLocal 相关方法的介绍,接下来分析ThreadLocal会不会出现内存泄漏

2.1、首先看一下 ThreadLocalMap 里面的Entity 的定义,Entity 里面的key是 弱引用了 threadLocal

static class 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.         */        static class Entry extends WeakReference<ThreadLocal> {            /** The value associated with this ThreadLocal. */            Object value;            Entry(ThreadLocal k, Object v) {                super(k);                value = v;            }        }}
在threadlocal的生命周期中,都存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.

就是因为这个弱引用,有人认为ThreadLocal会引发内存泄露,他们的理由是这样的:
    如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

 Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄露。


下面我们看一下 ThreadLocal 的set()方法,大体思路就是:

1.先取到一个Entity,

2.判断Entity.key是否是所需要的

3.如果是直接返回

4.如果不是,判断Entity.key 是否为空


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;

//这里是将长度和HashCode进行位运算,其实就是对Len取余

int i = key.threadLocalHashCode & (len-1);
// 使用线性探测法查找元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal k = e.get();
if (k == key) {
e.value = value; return;
}
if (k == null) {
replaceStaleEntry(key, value, i); // 这里就是清除 key 为 null 的情况
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;

// 当size 大于 阀值的时候,其实还是会再次清除key为null


if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

关于 set 方法,有几点需要地方:

①、 int i = key.threadLocalHashCode & (len-1);这里实际上是对 len 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。

接着这里每次调用set()时,值 i 都是会变,这一点比较重要,刚开始我也有点迷糊,我当初认为如果真的存在key 为null 的情况,并且每次get()获取Value 的时候,第一次就获取到或者在key 为null 的位置之前获取到,这样不是永远都不会清楚掉key 为null 的情况,这样不就会造成内存溢出,其实是不会的,key.threadLocalHashCode 会一直在变,注释上也说明了,会自动跟新( the next hash code to be given out ,Updated atomically)


private final int threadLocalHashCode = nextHashCode();
/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

从上面源码可以看出,我们每次获取threadLocalHashCode 的值得时候,都是调用 nextHashCode()方法,而 nextHashCode() 方法每次都是在AtomicInteger 变量(初始值为0)的基础上自动加一个 HASH_INCREMENT (0x61c88647),这样每次获取的位置都会在变,而且还是跳跃式变化,只要调用的次数够多,一定能够将key =null 的数据 清除。


接着再说一下 0x61c88647 这个数字,这是一个神奇的数字,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里,没有一点冲突,十分均匀。下面为测试代码:

public static void main(String[] args) {
AtomicInteger nextHashCode = new AtomicInteger();
int HASH_INCREMENT = 0x61c88647;
int size = 64;
List <Integer> list = new ArrayList <Integer> ();
for (int i = 0; i < size; i++) {
list.add(nextHashCode.getAndAdd(HASH_INCREMENT) & (size - 1));
}
System.out.println("排序前:" + list);
Collections.sort(list);
System.out.println("排序后: " + list);
}

分别将size 设置为 16,32,64 测试结果:

//size=16
排序前:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
//size=32
排序前:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
//size=64
排序前:[0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57]
排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]


从结果可以看出,真的是均匀分布,对于这个数字,我们暂时不去深究,只能说一句太神奇了.

②、replaceStaleEntry 和 cleanSomeSlots 方法中都会清理一些key 为null的 数据

③、当size 大于阀值的时候,也是会先清除一些key 为null的 数据,再判断清理后的大于是否大于阀值的3/4,如果仍然大于,进行扩容操作,如果小于便暂时不扩容.


从set()方法可以看出我们已经有了防止内存泄漏的机制,接下来我们再看一下 getEntity()方法是否也做了相应的处理,源码:

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);
}

从 getEntity() 方法可以看出,也是先去取一个值是否是所需要的,如果不是,调用getEntryAfterMiss(),相应的源码如下:

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;
}

可以看到在getEntryAfterMiss() 方法里面,会对取到的Entity 判断,如果是所需要的,直接返回,如果不是,判断key 是否为null,如果为null ,调用 expungeStaleEntry() 将其清除,如果不为null,继续下一次循环,直到Entity 为null.


从上面分析来看,在我们调用get(),set()方法时,都是会不断的检查是否存在 key= null 的情况,如果存在就将其清除.

那么Entry内的value也就没有强引用链,自然会被回收。所有说 只要还在调用get(),set()方法 ,就是不会发生内存溢出的问题.(如果有也最多有一个对象泄漏,即最后一次设置的那个值,因为在以后再也没有调用过任何方法,而且线程一直没有结束,如永远放在线程池里面). 所以为了不让这种情况发生,建议手动调用ThreadLocal的remove函数来释放,
接下来我们来看一下 remove() 方法:

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();
expungeStaleEntry(i);
return;
}
}
}

public void clear() {
this.referent = null;
}

从这个源码,我们看到了什么? remove() 方法分为了两步:

1.将 Entry 的键值Key设为 null,

2.调用 expungeStaleEntry 清理陈旧的 Entry。


那些 认为 key 为null 会造成内存泄漏的,看到 remove() 方法就是分为两步,第一步就是 将 键值Key设为 null ,是不是有点崩溃,而我们在调用 get(),set() 方法时,就是在 执行 remove() 里面的第二步。



总结:

1.Thread正常结束,就算 ThreadLocal 设置为null,不会内存泄漏,因为 ThreadLocalMap 是Thread 里面的一个属性,Thread 销毁,ThreadLocalMap 也不再存在.

2. Thread 放在线程池里面,不销毁,ThreadLocal 设置为null,如果还在一直调用 set(),get() 也是不会出现内存泄漏的情况,因为

get(),set() 里面 有 检查 key =null 的机制,如果发现会清除.

3. Thread 放在线程池里面,不销毁,ThreadLocal 设置为null ,如果以后再也不调用set(),get(),remove() 等方法,就是以后再也不用了,那么会出现内存溢出,但最多有一个对象泄漏,即最后一次设置的那个值,因为在以后再也没有调用过任何方法,而且线程一直没有结束,如永远放在线程池里面。




部分总结来自如下文章,他总结得比我精辟太多.参考:

http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/




2 0
原创粉丝点击