资讯专栏INFORMATION COLUMN

追踪解析 ThreadLocal 源码

wawor4827 / 933人阅读

摘要:虽然类名中带有字样,但是实际上并不是接口的子类。是弱连接接口,这意味着如果仅有指向某一类,其任然有可能被回收掉。这里使用弱连接的意义,是为了防止业务代码中置空对象,但是由于存在连接可达,所以仍然无法回收掉该对象的情况发生。

零 前期准备 0 FBI WARNING

文章异常啰嗦且绕弯。

1 版本

JDK 版本 : OpenJDK 11.0.1

IDE : idea 2018.3

2 ThreadLocal 简介

ThreadLocal 是 java 多线程中经常使用到的缓存工具,被封装在 java.lang 包下。

3 Demo
import io.netty.util.concurrent.FastThreadLocal;

public class ThreadLocalDemo {

    public static void main(String[] args) {

        //jdk 的 ThreadLocal
        ThreadLocal tl = new ThreadLocal<>();
        long tlBeginTime = System.nanoTime();
        //set(...) 方法存入元素
        tl.set("test");
        //get() 方法获取元素
        String get = tl.get();
        System.out.println("tl before remove: " + get);
        //remove() 方法删除元素
        tl.remove();
        get = tl.get();
        System.out.println("tl after remove: " + get);
        System.out.println(System.nanoTime() - tlBeginTime);

        //以下代码为著名 io 框架 Netty 的 FastThreadLocal 类的使用
        //FastThreadLocal,基本的使用方法和 ThreadLocal 没有区别
        //FastThreadLocal 的实例对象创建比较慢,但是元素的获取、增、删的性能很好
        FastThreadLocal fastTl = new FastThreadLocal<>();
        long fastTlBeginTime = System.nanoTime();
        fastTl.set("test");
        String fastGet = fastTl.get();
        System.out.println("tl2 before remove: " + fastGet);
        fastTl.remove();
        fastGet = fastTl.get();
        System.out.println("tl2 after remove: " + fastGet);
        System.out.println(System.nanoTime() - fastTlBeginTime);

        //此处的 Netty 使用 4.1.33.Final 的版本
        //笔者跑了一下,FastThreadLocal 的增删查操作大概比 ThreadLocal 快十倍
        //但是此处仅为简陋测试,并不严谨
    }
}

FastThreadLocal 的源码暂不展开,将来有机会多带带开一章去学习。这里先理解 ThreadLocal。

一 ThreadLocalMap

在了解 ThreadLocal 的全貌之前先来理解一下 ThreadLocalMap 类。

其为 ThreadLocal 的静态内部类。虽然类名中带有 map 字样,但是实际上并不是 Map 接口的子类。

ThreadLocalMap 本质上是数组。每个 Thread 实例对象都会维护多个 ThreadLocalMap 对象:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

但是需要注意的是,在默认情况下,线程对象的 ThreadLocalMap 对象们都是未初始化的,需要使用 createMap(...) 方法去初始化:

//ThreadLocal.class
void createMap(Thread t, T firstValue) {
    //此处 ThreadLocal 将自身作为 key 值存入了 map 中
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以想到的是,此处是为了提高线程的性能,而设计了一个懒加载(Lazy)的调用模式。

[但是实际上这是理想情况,对于主线程来说,Collections、StringCoding 等的工具类在 jdk 加载时期就会调用 ThreadLocal,所以 ThreadLocalMap 肯定会被创建好]

再来看一下 ThreadLocalMap 的构造方法:

//ThreadLocalMap.class
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    //Entry 是 ThreadLocalMap 的静态内部类,代表节点的对象
    //table 是一个 Entry 数组,代表链表
    table = new Entry[INITIAL_CAPACITY];
    //这里调用 key 的 hash 值进行数组下标计算
    //INITIAL_CAPACITY 为常量 16
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //threshold = INITIAL_CAPACITY * 2 / 3
    setThreshold(INITIAL_CAPACITY);
}
Entry

Entry 是 ThreadLocalMap 的静态内部类,本质上是数组的节点 value 的封装:

static class Entry extends WeakReference> {
    //储存的 value 值
    Object value;

    Entry(ThreadLocal k, Object v) {
        //调用父类的方法,会将 ThreadLocal 存入 Reference 中的 referent 对象中
        super(k);
        value = v;
    }
}

由上可知 Entry 继承了 WeakReference。WeakReference 是弱连接接口,这意味着如果仅有 Entry 指向某一 ThreadLocal 类,其任然有可能被 GC 回收掉。

这里使用弱连接的意义,是为了防止业务代码中置空 ThreadLocal 对象,但是由于存在连接可达,所以仍然无法回收掉该对象的情况发生。

即可以这么说,如果使用者在业务代码中存在可达的强连接引用对象,那么 ThreadLocal 永远不会被 GC 清理掉;但是如果强连接消失了,那么弱连接并不能保证它一定存活。当然换句话说,强连接消失的时候,证明使用者已经不需要这个对象了,那么它被消灭也是应该的。
二 存入元素

来看一下 ThreadLocal 的 set(...) 方法:

//step 1
//ThreadLocal.class
public void set(T value) {
    //获取当前线程的实例对象
    Thread t = Thread.currentThread();
    //通过实例对象获取到 map
    //map 实际上是定义在 Thread 类中的 ThreadLocalMap 类型的对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //存入元素
        map.set(this, value);
    } else {
        //如果 map 不存在,会在这里创建 map
        createMap(t, value);
    }
}

//step 2
//ThreadLocalMap.class
private void set(ThreadLocal key, Object value) {

    //获取数组 table
    Entry[] tab = table;
    //获取长度
    int len = tab.length;
    //根据 hash 值算出下标
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        //nextIndex(...) 方法获取数组的下一个下标的元素
        //基本等同于 i + 1,但是一般情况下不需要用到

        //从节点中获取 ThreadLocal 对象
        ThreadLocal k = e.get();

        //正常情况下 k == key,第一次存值的时候 value = null
        if (k == key) {
            e.value = value;
            return;
        }

        //正常情况下不会出现
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //进入此处语句的条件是 k 并不为 null,且 key 不等于数组内现存的所有 ThreadLocal
    //则在此处符合要求的下标处新建一个节点,并添加到 table 数组中
    //注意,这里其实是覆盖操作,会覆盖掉之前在此下标处的节点
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
三 获取元素

来看一下 ThreadLocal 的 get() 方法:

//step 1
//ThreadLocal.class
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;
        }
    }
    //map 为 null 的情况下会进入该方法
    //此处会将 null 作为 value,当前 ThreadLocal 作为 key,传入 ThreadLocalMap 中
    return setInitialValue();
}

//step 2
//ThreadLocalMap.class
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
        //此处会轮询整个数组去寻找,实在找不到会返回 null
        return getEntryAfterMiss(key, i, e);
}

基本逻辑和 set(...) 方法差不多,不多赘述。

四 移除元素

来看一下 ThreadLocal 的 remove() 方法:

//step 1
//ThreadLocalMap.class
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        //调用 ThreadLocalMap 的 remove(...) 方法
        m.remove(this);
    }
}

//step 2
//ThreadLocalMap.class
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)]) {
        //此处是一个和 set(...) 中很像的轮询方法
        //比对 key 值,如果相等的话会调用 clear() 方法清理掉
        if (e.get() == key) {
            e.clear();
            //此方法用于清理 key 值为 null 的节点
            expungeStaleEntry(i);
            return;
        }
    }
}

//step 3
//Reference.class
public void clear() {
    //Reference 是 WeakReference 的父类,即也就是 Entry 的父类
    //将值置空
    this.referent = null;
}
五 ThreadLocal 的 hash 值

上述方法多次使用到了用 hash 去计算数组下标的操作。如果不同 ThreadLocal 的 hash 值相同,那么就会造成计算出来的下标相同,会相互影响存入的值。

所以 ThreadLocal 的 hash 值一定不能相同。

在 ThreadLocal 中,hash 值是一个 int 类型的变量:

private final int threadLocalHashCode = nextHashCode();

其调用了静态方法 nextHashCode() 去产生 hash 值:

//ThreadLocal.class
private static int nextHashCode() {
    //HASH_INCREMENT = 0x61c88647 (一个很神奇的用来解决 hash 冲突的数字)
    //nextHashCode 是一个定义在 ThreadLocal 中的静态 AtomicInteger 类型变量
    //getAndAdd(...) 方法会每次给 nextHashCode 的值加上 HASH_INCREMENT 的值,并返回最终的相加结果值
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

jdk9 以后官方应该比较希望使用 VarHandler 类来取代 Atomic 类,所以在不久的未来,很可能相关方法会有一些变动。

六 一点唠叨

ThreadLocal 的源代码还是比较简洁的,方法封装不多,读起来不算费劲,有一些算法层面的东西比较麻烦,但是不影响阅读。

Netty 的 FastThreadLocal,其设计就要比 ThreadLocal 复杂得多,有机会再深入学习。

本文仅为个人的学习笔记,可能存在错误或者表述不清的地方,有缘补充

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/73304.html

相关文章

  • 追踪解析 Netty 的 FastThreadLocal 源码

    摘要:零前期准备文章异常啰嗦且绕弯。二是底层真正起作用的类,并且提供了大量的静态方法。在普通的线程中,这个对象由于本身没有的原生支持,所以只能附着在对象当中。同一个线程中如果创建多个对象,获取到的是同一个。 零 前期准备 0 FBI WARNING 文章异常啰嗦且绕弯。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 Netty 版本 : net...

    Anonymous1 评论0 收藏0
  • 追踪解析 FutureTask 源码

    摘要:零前期准备文章异常啰嗦且绕弯。版本版本简介是中默认的实现类,常与结合进行多线程并发操作。所以方法的主体其实就是去唤醒被阻塞的线程。本文仅为个人的学习笔记,可能存在错误或者表述不清的地方,有缘补充 零 前期准备 0 FBI WARNING 文章异常啰嗦且绕弯。 1 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 2 ThreadLocal 简介 ...

    xcc3641 评论0 收藏0
  • 追踪解析Gson源码(2)

    摘要:接上篇三和在进行的序列化和反序列化源码解析之前先了解一下其主体工具类。是中用于序列化的主体。同时为了兼顾性能做了很多有意思的设计,比如获取适配器的时候的双缓存设计,应该是为了提高解析器的复用效率,具体有待研究。 接上篇 三 JsonReader 和 JsonWriter 在进行 json 的序列化和反序列化源码解析之前先了解一下其主体工具类。 1 JsonReader JsonRead...

    shiguibiao 评论0 收藏0
  • node.js与ThreadLocal

    摘要:变量的说法来自于,这是在多线程模型下出现并发问题的一种解决方案。目前已经有库实现了应用层栈帧的可控编码,同时可以在该栈帧存活阶段绑定相关数据,我们便可以利用这种特性实现类似多线程下的变量。 ThreadLocal变量的说法来自于Java,这是在多线程模型下出现并发问题的一种解决方案。ThreadLocal变量作为线程内的局部变量,在多线程下可以保持独立,它存在于线程的生命周期内,可以在...

    jasperyang 评论0 收藏0

发表评论

0条评论

wawor4827

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<