jdk你不得不了解的实现:ThreadLocal源码全解析

本文原创地址,我的博客https://jsbintask.cn/2019/04/01/jdk/jdk8-threadlocal/(食用效果最佳),转载请注明出处!

前言

ThreadLocal是jdk中一个非常重要的工具,它可以控制堆内存中的对象只能被指定线程访问,如果你经常阅读源码,基本在各大框架都能发现它的踪影。而它最经典的应用就是事务管理,同时它也是面试中的常客。

原理

我们知道,堆内存是共享的,为什么ThreadLocal能够控制指定线程访问呢? 如图:
ThreadLocal

  1. 调用ThreadLocal的get方法。
  2. 获取当前线程t1.
  3. 获取t1的成员变量ThreadLocalMap
  4. 根据ThreadLocal的hashcode计算出ThreadLocalMap中Entry[]数组的索引。
  5. 返回索引位置的值。
    这样我们就很容易理解了,为什么只有当前线程才能获取到某些值,因为这是这些值都直接保存在当前线程的成员变量ThreadLocalMap中,而ThreadLocal在这个过程中充当的角色则是提供它独一无二的hashcode值,这样我们就能计算出我们保存的值在ThreadLocalMap的位置。

源码分析

我们从构建一个ThreadLocal到调用它的set,get方法完整的分析一遍它的源码。

构造器

当我们使用new ThreadLocal<>() new一个ThreadLocal对象时,它初始化了一个成员变量threadLocalHashCode,这个成员变量代表当前ThreadLocal的hashcode值,而它肯定是唯一的:
ThreadLocal

  1. ThreadLocal内部有一个静态hashCode生成器nextHashCode
  2. 每次新new一个ThreadLocal对象,调用这个生成器同步方法获取hashcode。
    因为依赖于静态成员变量nextHashCode的关系,所以它的hashcode肯定唯一!

    set(T t)

    1
    2
    3
    4
    5
    6
    7
    8
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }
  3. 获取当前线程t。

  4. 从t中获取ThreadLocalMap map。
    ThreadLocal
  5. 如果map不为空,将当前值value放入map。
  6. 如果map为空,新建一个ThreadLocalMap放入线程t。
    ThreadLocalMap是ThreadLocal中的内部类,它的结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

    private Entry[] table;

    private int size = 0;

    private static final int INITIAL_CAPACITY = 16;

    private int threshold; // Default to 0
    }

类似于ArrayList内部的构造,它内部有一个Entry数组table,并且Entry继承自弱引用,所以每一个Entry中保存着两个值,ThreadLocal,value,value既是我们要保存的值。
接着,我们回过头详细分析第三步,ThreadLocalMap的set方法:

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
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 1

for (Entry e = tab[i];
e != null; // 2
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) { // 3
e.value = value;
return;
}

if (k == null) { // 4
replaceStaleEntry(key, value, i);
return;
}
}

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

  1. 根据ThreadLocal的hashCode计算出在entry中的索引i。
  2. 取出i对应的Entry值e。
  3. 如果e的key等于当前ThreadLocal,代表已经有一个一样的ThreadLocal在这个entry设值,直接替换这个entry上的value。
  4. e上面的ThreadLocal为null,代表垃圾收集器准备回收这个Entry了,重新计算数组大小,重新hash。
  5. i位置还没有初始化(第一次set这个ThreadLocal),直接将value放到i的位置。
  6. 扩容Entry数组。

get()

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
  1. 获取当前线程。
  2. 从当前线程中获取ThreadLocalMap
  3. 从ThreadLocalMap中找出ThreadLocal对应的Entry.
  4. 如果Entry不为null,直接返回Entry中的value
  5. 返回初始值。
    其中,ThreadLocalMap的get(ThreadLocal tl)如下:
    ThreadLocal
    它和我们一开始的分析一样,根据ThreadLocal的hashcode成员变量计算出索引位置i,得到Entry。这里同样有特殊情况,如果得到的Entry的key和当前ThreadLocal不相等,代表这个Entry将被垃圾收集处理,调用getEntryAfterMiss rehash,计算数组大小。

注意事项

从上面的代码分析中,我们知道,ThreadLocalMap的生命周期和当前线程同步,如果当前线程被销毁,则map中的所有引用均被销毁。但如果当前线程不被销毁呢(线程池,tomcat处理请求等)?Entry中保存了ThreadLocal的弱引用以及value,gc时可能清理掉ThreadLocal,而这个value确再没有访问之地,这个时候就会造成内存泄漏!
所以我们需要手动调用remove方法清理掉当前线程ThreadLocalMap的引用!

总结

  1. ThreadLocal中真正保存的值还是在线程的ThreadLocalMap中,ThreadLocal只是使用它的hashcode值充当中间计算变量。
  2. ThreadLocalMap内部使用一个Entry数组保存数据。
  3. ThreadLocal可能出现内存泄漏的情况,最好手动调用remove方法。

关注我,这里只有干货!

×

谢谢你支持我分享知识

扫码支持
扫码打赏,心意已收

打开微信扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 前言
  2. 2. 原理
  3. 3. 源码分析
    1. 3.1. 构造器
    2. 3.2. set(T t)
    3. 3.3. get()
  4. 4. 注意事项
  5. 5. 总结
欢迎扫描左方二维码跟作者交流.