threadLocal源码解析

简单记录一些看threadLocal时遇到比较有意思的点。

ThreadLocalMap

ThreadLocal的数据, 存放在Thread的属性ThreadLocal.ThreadLocalMap threadLocals

ThreadLocalMap可以看做一个简单的map<ThreadLocal, Object>, 他保存了每一个ThreadLocal和对应的值.
如有两个ThreadLocal

ThreadLocal<Integer> a = new ThreadLocal<>();
ThreadLocal<Integer> b = new ThreadLocal<>();

在t1线程中设置值:

a.set(1);
b.set(2);

可以想象为t1.threadLocals[a] = 1, t1.threadLocals[b]=2

对于不同的Thread, 每一个Thread中threadLocals分别保存了ThreadLocal和他们对应的值.
如上面栗子中, 如果另一个线程t2也设置了

a.set(3);
b.set(4);

那么可以想象为t2.threadLocals[a] = 1, t2.threadLocals[b]=2

为什么不要直接在ThreadLocal中使用一个map存储对应的线程和值?
如a.map[t1] = 1, a.map[t2] = 3, b.map[t1] = 2, b.map[t2] = 4
我想, 应该是为了方便内存回收.
使用Thread.threadLocals方式, 如果线程结束了, 那该线程set的值(如果没有其他引用)就可以被回收了.
如果使用ThreadLocal.map模式, 已消亡Thread set的值会一直停留在内存中.(map.key会一直指向消亡的Thread)

当线程退出时, Thread类会进行一些清理工作, 其中就包括ThreadLocalMap

/**
 * 在线程推出前, 由系统回调,进行资源清理
 **/
private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    target = null;
    // 加速ThreadLocalMap清理
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
} 

WeakReference<ThreadLocal<?>>

还有一个值得注意的点, ThreadLocalMap中的数据是存储在Entry[] table中,
Entry的定义是

static class Entry extends WeakReference<ThreadLocal<?>> {
    // 这个valu就是ThreadLocal.set的值
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

注意这里使用了WeakReference, 而不是Entry extends ThreadLocal<?>.
WeakReference引用表明, 一旦没有指向 referent 的强引用, weak reference 在 GC 后会被自动回收.

先看一个小栗子:

ThreadLocal<Object> local = new ThreadLocal<Object>() {
    protected void finalize() throws Throwable {
        System.out.println(this.toString() + " threadLocal is gc");
    }
};

Object reference = new Object() {
    protected void finalize() throws Throwable {
        System.out.println(this.toString() + " object is gc");
    }
};

local.set(reference);

// 去掉强引用
local = null;   // 代码1
System.gc();

System.out.println(t);  // 代码2

输出结果

multi.ThreadLocalTest$1@1714d2a threadLocal is gc
gc finish!!

要通过debug查看Thread.threadLocals的情况.
debug到代码1处, 可以看到

1.png

debug到代码2处, 可以看到

2.png

可以看到, gc后, entry的referent被回收了, 这时ThreadLocal可以被内存回收了, 如果使用
Entry extends ThreadLocal<?>, 那么entry.key会一直引用ThreadLocal, 导致ThreadLocal无法被回收.

值得注意的是,local虽然回收了, 但 reference对象 并没有回收, 哪怕你在gc前将其设为nullreference = null;, 因为threadLocals.entity中的value值依然引用它, 这点可能会造成内存泄露。想要及时回收它, 可以如下操作

// 去掉强引用
reference = null;
local.set(null);
local = null;
System.gc();

输出结果

multi.ThreadLocalTest$2@1714d2a object is gc
multi.ThreadLocalTest$1@1b6b5b4 threadLocal is gc
gc finish!!

那么除了内存回收, 使用WeakReference还有没有其他意义呢?
我们可以看一下ThreadLocalMap的实现, ThreadLocalMap的数据存放在Entry[] table数组中, 通过hash算法实现一个类Map的数据结构,就来看一下ThreadLocalMap.set方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算hash值
    int i = key.threadLocalHashCode & (len-1);

    // 找到可以set的位置
    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);
            return;
        }
    }

    // set 值
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();   // 扩容
}

关键在查找可以set的位置上, 使用hash算法无可避免会遇到hash冲突, 常见的解决方法,
如链路法, 在该位置使用一个链路存储多个冲突的值(HashMap的方法),
这里使用的是线性补偿探测法, 发生冲突是, 使用当前位置继续进行hash计算(nextIndex(i, len)方法), 直到找到一个可以使用的位置.

上面栗子中,

  • 如果entry为null, 可以直接创建entry放置到该位置.
  • 如果entry.referent(就是entry指向的ThreadLocal) == key, 可以直接替换掉value
  • 如果entry.referent==null, 这就是上面提到的, referent指向的ThreadLocal没有强引用了,所以referent被GC回收了, 既然这时ThreadLocal没有引用了, 所以这里就可以考虑替换该entry了, 不用再继续寻找合适的位置了.

所以这里使用WeakReference, 还有一个意义就是减少hash冲突.

看到这里, 不得不说ThreadLocal的实现, 还真是"有点意思"啊

错误之处, 还望指出

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