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在table中的位置
    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!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容