ThreadLocal源码解析

Posted miaomiaoLoveCode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码解析相关的知识,希望对你有一定的参考价值。

引言


ThreadLocal,线程变量,线程可以将本次线程内经常使用的变量存储到ThreadLocal中,方便本次线程内其他的操作使用。

注:特别需要注意的是,有些博客说ThreadLocal可以保证线程安全,这是错误的认识,ThreadLocal存储的只是每一个线程的本地变量,并未涉及到临界区,不能保证线程安全。在使用的时候一定要注意使用场景,ThreadLocal存储的应该是每个线程内部共享的一些数据,而非临界区数据。


ThreadLocal源码解析

用过ThreadLocal的程序员们可能都知道,ThreadLocal最常用的三个方法无非就是get(),set(T value)和remove(),本文就这三个常用的方法来分析ThreadLocal的相关源码。

  • ThreadLocalMap

在分析具体操作源码之前,先看看ThreadLocal底层的存储结构ThreadLocalLocalMap吧,这里着重讲解它的get/set/remove操作,关于其他部分的源码,有兴趣的读者可以自行阅读。

get操作

private Entry getEntry(ThreadLocal key) 
    //找到key在hashEntry中的位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //找到的值是当前查询的key的值,返回
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);

从上面的源码可以看出,假如直接hash找到了对应的slot,返回即可,假若没找到呢。。。没找到执行下面的操作:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) 
    Entry[] tab = table;
    int len = tab.length;
    //在table里查询,如若找到待查询entry,返回该entry
    while (e != null) 
        ThreadLocal k = e.get();
        //entry的key为待查询key
        if (k == key)
            return e;
        //entry的key为null时,表明当前的entry并不是最新的,需要做相关的操作删除掉非最新的entry
        if (k == null)
            expungeStaleEntry(i);
        else
            //i++,i == len 时 i = 0
            i = nextIndex(i, len);
        e = tab[i];
    
    return null;

set操作

//set操作,key为当前ThreadLocal对象,value为对应的变量值
private void set(ThreadLocal key, Object value) 
    Entry[] tab = table;
    int len = tab.length;
    //hash出当前key所在slot
    int i = key.threadLocalHashCode & (len-1);

    //处理tab[i] != null
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) 
        ThreadLocal k = e.get();

        //key存在,覆盖value
        if (k == key) 
            e.value = value;
            return;
        
        //key不存在,需要在table对应位置插入entry
        if (k == null) 
            replaceStaleEntry(key, value, i);
            return;
        
    

    //tab[i] == null,直接做插入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //sz >= len * loadFactor(loadFactor = 2/3)需要resize
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();

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)]) 
        //找到对应的entry
        if (e.get() == key) 
            //clear entry
            e.clear();
           //删掉该entry
            expungeStaleEntry(i);
            return;
        
    

ThreadLocalMap的相关重要操作到这里就分析完毕,ThreadLocal的相关操作都是在ThreadLocalMap操作基础上封装的。

  • ThreadLocal.get()
public T get() 
    Thread t = Thread.currentThread();
    //获取当前线程的threadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) 
        //entry不为null,返回对应的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    
    //否则,返回null
    return setInitialValue();
  • ThreadLocal.set(T value)
public void set(T value) 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //当前线程的threadLocalMap已创建,直接往map中set值
    if (map != null)
        map.set(this, value);
    //当前线程的threadLocalMap未创建,需要先创建再set
    else
        createMap(t, value);
  • ThreadLocal.remove()
public void remove() 
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);

后记

到这里为止,ThreadLocal的相关源码分析就结束了。在最后还需要讲讲之前我在使用ThreadLocal遇到的坑。

在业务线工作的时候,发现很多时候都有线程内共享变量的需求,ThreadLocal最适合这个场景,但是在使用ThreadLocal的时候,总是忘记remove,终于出现了一次巨大的故障。。。ThreadLocal和ThreadPoolExcutor一起使用时,出现了内存泄漏,在那一刹那,机器内存使用率蹭蹭往上涨,排查了很久,才发现是ThreadLocal忘记remove了,为什么ThreadLocal不remove会造成内存泄漏呢?

ThreadLocal导致的内存泄漏

之前查过很多资料,都是ThreadLocal不remove不会导致内存泄漏,但是事实却不是如此,血一般的教训啊。

每个Thread都有一个ThreadLocalMap,虽说它是弱引用,但是弱引用仅仅都是针对key,每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null的时候,没有任何的强引用指向ThreadLocal实例,ThreadLocal将会被gc回收,但是,value却不能被回收,因为存在一条从当前线程连接过来的强引用,只有当前
thread结束以后,thread,map,value将会全部被gc回收。

针对这个问题,ThreadLocal为了减少内存泄漏的机会,在get/set的时候都会检查ThreadLocalMap中是否有key == null的value,会把这部分value删除掉。程序员们可能这时候有这个疑惑,既然都这么做来避免了,为什么会出现内存泄漏啊?但是,大家就没想过么,假若线程不被销毁,它的ThreadLocal的get/set也一直未被使用,这不就出现了实际意义上的内存泄漏么?刚好线程池里的一部分核心线程是不会被销毁的,假若出现这种情况是一定会出现内存泄漏的!!!所以,在使用ThreadLocal结束之后一定不要忘记remove!!!

以上是关于ThreadLocal源码解析的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal源码解析

JDK1.8中ThreadLocal源码解析

JDK1.8中ThreadLocal源码解析

Java 8 ThreadLocal 源码解析

ThreadLocal源码解析

ThreadLocal源码解析