追踪解析 ThreadLocal 源码
零 前期准备
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<String> 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<String> 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<ThreadLocal<?>> { //储存的 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 复杂得多,有机会再深入学习。
本文仅为个人的学习笔记,可能存在错误或者表述不清的地方,有缘补充