【ThreadLocal】ThreadLocal的实现机制和原理
1 前言
这节我们看下 ThreadLocal ,这个东西大家应该不陌生,经常在一些同步优化中会使用到它。很多地方叫线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。也就是对于同一个ThreadLocal,每个线程通过get、set、remove接口操作只会影响自身线程的数据,不会干扰其他线程中的数据。常见的比如我们的登录信息是不是用到了,AOP里的 AOPContext等,那么这节我们就来看看它的实现机制。
2 类图
说到 ThreadLocal ,涉及的相关类我们先简单介绍下:
- ThreadLocal 核心类,本地线程类,他里边有两个子类:SuppliedThreadLocal这个主要是传的表达式相当于延迟初始化,当调用get的时候才会获取值;ThreadLocalMap这个就是用来存放数据管理数据的,会依附在 Thread 里;
- Thread 线程类 内部拥有ThreadLocalMap变量,有两个,threadLocals 负责正常存放某个本地线程变量,inheritableThreadLocals负责父子线程传递的;
- InheritableThreadLocal 用于父子线程的传递
我们看下类图:
3 源码分析
3.1 测试代码
public class TestThreadLocal { // Person类 static class Person { private String name; Person(String name) { this.name = name; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + '}'; } } public static void main(String[] args) { Person person = new Person("狗子"); ThreadLocal<Person> personThreadLocal = new ThreadLocal<>(); personThreadLocal.set(person); System.out.println(personThreadLocal.get()); } }
可以看到我们测试的代码,创建 ThreadLocal 对象,设置值以及获取,那么接下来我们就来看看 set、get方法。
3.2 set 方法
// TheradLocal public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程中的 map 集合 ThreadLocalMap map = getMap(t); if (map != null) /** * 如果 map 不为空,就设置值 * 可以看到 key 就是我们的 ThreadLocal本身 说明什么? * 也就是说我们的每个线程中只会存放一个 ThreadLocal对象对应的值 */ map.set(this, value); else /** * map 为空的话 就要创建并赋予 value 初值 */ createMap(t, value); }
// 其实就是 Thread 中的 threadLocals 变量 ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
// Therad public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }
可以看到我们的 set 方法,首先会获取当前线程的 threadLocals,如果为空的话就会创建并赋予初值,不为空的话就会把当前值设置进去,那我们先看一下 createMap 方法看看是如何初始化并赋值的。
3.2.1 createMap 方法
// ThreadLocal void createMap(Thread t, T firstValue) { // 就是创建 ThreadLocalMap 对象,并赋值给线程 t t.threadLocals = new ThreadLocalMap(this, firstValue); }
那我们继续看下 ThreaLocalMap 的实例化:
// ThreadLocalMap ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { /** * 实例化 table 数组,初始大小为:16 * private static final int INITIAL_CAPACITY = 16; */ table = new Entry[INITIAL_CAPACITY]; /** * 就是根据 ThreadLocal的哈希值和数组的长度取余 * 那你有没有疑问 取余的话,会不会不同的ThreadLocal取余一样不就冲突了? * 其实 threadLocalHashCode 是递增的,这个你们可以看下我就不在这里看了哈,源码就在上边 用了atomic的增长的 */ int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 设置值 key->我们的ThreadLocal对象 value->就是我们要存放的值 table[i] = new Entry(firstKey, firstValue); // 因为创建并放置了一个键值对所以长度 1 size = 1; // 扩容标志 也就是到达三分之二的时候要进行重新计算扩容 setThreshold(INITIAL_CAPACITY); }
整体上就是实例化我们的ThreaLocalMap ,里边实际上就是有一个 Entry数组来维护,初始化长度为 16,扩容标志是三分之二的水平线。那我们再来简单看下我们的Entry:
// ThreadLocalMap的内部类 // 可以看到我们的 Entry 是继承的弱引用,也就是线程中的 ThreadLocalMap 不会影响 ThreadLocal的回收 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ // 我们存放的值 Object value; // k 就是我们的 ThreadLocal对象,弱引用 v就是我们要存放的值 Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
可以看到 ThreaLocalMap 弱引用于我们的 ThreadLocal 对象,也就是线程中的 ThreaLocalMap 不会影响我 ThreadLocal 对象的回收。
3.2.2 ThreaLocalMap 中的 set 方法
我们继续看下当线程中的 ThreadLocalMap不为空的话,是如何存放的,看之前首先我们猜猜应该有哪些,我们看到实例化的时候有扩容标志,那么是不是会在 set 的时候来进行扩容的判断呢?是不是我们来看看:
/** * @param key 我们的 ThreadLocal 对象 * @param value 就是我们要存放的值 */ private void set(ThreadLocal<?> key, Object value) { // 我们的 Entry 数组 Entry[] tab = table; // 数组的长度 int len = tab.length; // 取余,判断当前 ThreadLocal 应该存放的索引位置 int i = key.threadLocalHashCode & (len-1); /** * 获取索引下的 Entry 键值对 这里为什么用循环呢? * threadLocalHashCode 递增的值是 nextHashCode.getAndAdd(HASH_INCREMENT) 0x61c88647 * 并不是按1增长的 所以可能还是会冲突吧 * 所以冲突了就会加1 nextIndex => ((i + 1 < len) ? i + 1 : 0) * 循环的作用: * 1、处理索引冲突 * 2、处理脏的ThreadLocal */ for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取到我们的 ThreadLocal 对象 ThreadLocal<?> k = e.get(); // 当发现对应的 ThreadLocal 已经存在某个值,则进行替换并返回 if (k == key) { e.value = value; return; } // 因为 ThreadLocal 是弱引用的, // 那么当 ThreadLocal 被回收时,这里就会为空 // 那么说明我们当前 Entry中的ThreadLocal是脏的了,进行脏处理 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 索引位置的 Entry为空,那么创建 Entry 并设置 value tab[i] = new Entry(key, value); // 长度++ int sz = ++size; /** * cleanSomeSlots 清理脏的 ThreadLocal 发现好严谨奥 * 返回为true 说明有脏的 ThreadLocal * 为false的情况下,并且大小到达了我们的增长标志进行重新hash并扩容 */ if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
可以看到里边除了常规的替换值设置值,有两个很重要的操作就是清理脏 ThreadLocal - replaceStaleEntry 以及扩容- reHash 判断,其实 cleanSomeSlots 也是清理脏 ThreadLocal,调用的 expungeStaleEntry,我们先来看下 cleanSomeSlots :
3.2.2.1 cleanSomeSlots
// ThreadLocalMap private boolean cleanSomeSlots(int i, int n) { // 有效清理标志 boolean removed = false; // 我们的 Entry 数组 Entry[] tab = table; // 数组长度 int len = tab.length; do { // 下一个索引 i = nextIndex(i, len); // 获取索引位置的 Entry Entry e = tab[i]; // 判断当前的 Entry 的 ThreadLocal 是否为空 if (e != null && e.get() == null) { // TheradLocal为空 说明是脏的 n = len; // 更新有效清理标志 removed = true; // 清理脏 TheradLocal i = expungeStaleEntry(i); } // 无符号右移 相当于除以2 相当于折半判断并不是全都遍历一遍 } while ( (n >>>= 1) != 0); return removed; }
那我们继续跟进去看下 expungeStaleEntry,是如何清理的:
3.2.2.2 expungeStaleEntry
// ThreadLocalMap // staleSlot 脏ThreadLocal索引 private int expungeStaleEntry(int staleSlot) { // 我们的 Entry 数组 Entry[] tab = table; // 数组的长度 int len = tab.length; // expunge entry at staleSlot // 因为我们的 table 是强引用 value的 这里先把 value的强引用释放 tab[staleSlot].value = null; // 把自身的占位释放 tab[staleSlot] = null; // 长度-- size--; // Rehash until we encounter null Entry e; int i; // 遍历下一个索引位置的 Entry 是否为空 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 不为空的话判断 ThreadLocal 是否是脏的 ThreadLocal<?> k = e.get(); // 是脏的话 释放引用 长度-- if (k == null) { e.value = null; tab[i] = null; size--; } else { // ThreadLocal 不脏的情况下 因为前边释放了一个位置 这里再根据长度取余 int h = k.threadLocalHashCode & (len - 1); // 新的索引位置 不等于原来旧的索引位置的话 if (h != i) { // 旧位置置为空 tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. // 判断新位置是否为空,不为空的找一个空的位置出来 while (tab[h] != null) h = nextIndex(h, len); // 设置到新位置去 tab[h] = e; } /** * 我在想为什么要这么做呢? 为什么还要遍历其它的不为空呃 给他们重新变换位置呢? * 是为了收缩?还是就是单纯的要释放其它脏的ThreadLocal呢 */ } } return i; }
释放的主要操作就是 tab[i].value = null 释放强引用,并把 tab[i] = null, 其实释放就完事了,但是它还会接着遍历判断下一个索引位置的 Entry 是否也是脏的,是的话也会继续清理,不是的话会调整新位置,直到索引位置的 Entry 是空的话,结束循环完事。这么做的道理可能就是更好的去为 GC做准备释放一些无效的引用,防止内存泄漏嘛。
3.2.2.3 replaceStaleEntry
/** * ThreadLocalMap * @param key 脏的ThreadLocal * @param value 要存放的值 * @param staleSlot 脏索引 */ private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { // 我们的数组 Entry[] tab = table; // 数组的长度 int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). // slotToExpunge 要去释放的索引位置 int slotToExpunge = staleSlot; // 往前找一个Entry不空的 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) // 发现也是脏的 if (e.get() == null) // 更新要释放的索引位置 slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first // 又往后找一个 Entry不为空的 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 获取当前不为空的 Entry 的 ThreadLocal ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEn // to remove or rehash all of the other entries in run. // 发现就是我们参数里的脏ThreadLocal if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
replaceStaleEntry 这个方法说实话有点邪门,没怎么看懂,它这是先往前找一找脏ThreadLocal,然后又往后遍历,我搜了搜别人的理解,也是众说纷纭,暂时保留,总之我们知道这个也是清理脏ThreadLocal的。那我们看见有三个方法来清理脏ThreadLocal的我们看下三者的区别:
- expungeStaleEntry 这个肯定是会清理掉至少一个的,返回值返回的是下一个 空Entry的索引位置;
- replaceStaleEntry 这个方法从我的理解上我觉得时空清理至少一个的;
- cleanSomeSlots 这个是会尝试去清理脏ThreadLocal,有可能没清一个,有可能清了。
set 方法除了清理还有一项重要的是判断是否需要扩容,我们来看下:
3.2.2.4 reHash
// ThreadLocalMap private void rehash() { // 还是会调用 expungeStaleEntry 进行清理脏ThreadLocal expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis // 判断是否达到 四分之三 超过了就重新扩容 if (size >= threshold - threshold / 4) resize(); }
// ThreadLocalMap private void resize() { // 我们的数组 Entry[] oldTab = table; // 数组的长度 int oldLen = oldTab.length; // 新数组的长度,可以看到是2倍扩容 int newLen = oldLen * 2; // 创建新的数组 Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { // 脏的ThreadLocal就被丢弃了 ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { // 重新计算位置 int h = k.threadLocalHashCode & (newLen - 1); // 冲突的话重新获取就是往后放 while (newTab[h] != null) h = nextIndex(h, newLen); // 设置到新数组中 newTab[h] = e; // 计数器++ count++; } } } // 设置扩容标志 setThreshold(newLen); // 设置回去 size = count; table = newTab; }
其实我们默认的扩容水平线是:threshold = len * 2 / 3,也就是到达三分之二的时候会进入 reHash 方法,而 reHash会进行脏ThreadLocal的清理,清理后发现长度达到了 四分之三的话,就会扩容,扩容后的长度是原长度的2倍。
3.3 get 方法
看完 set 的方法,我们再来看下 get 的方法:
// ThreadLocal public T get() { // 当前线程 Thread t = Thread.currentThread(); // 当前线程中的 ThreadLocalMap ThreadLocalMap map = getMap(t); // 不为空的话,就从 ThreadLocalMap 取 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); // 取出的值不为空的话,进行强制转换 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // ThreadLocalMap 为空或者值为空的话,就从 setInitialValue 中获取 return setInitialValue(); }
// ThreadLocal private T setInitialValue() { // 从 initialValue 方法中获取默认的值 默认的实现是返回 null T value = initialValue(); // 当前线程 Thread t = Thread.currentThread(); // 获取到线程的 ThreadLocalMap ThreadLocalMap map = getMap(t); // 不为空的话 set 设置进去 if (map != null) map.set(this, value); else // 创建 ThreadLocalMap 并初始化值 createMap(t, value); return value; }
3.3.1 ThreadLocalMap 的 getEntry 方法
当线程中的ThreadLocalMap 不为空的情况下,会调用 ThreadLocalMap 中的 getEntry来获取,我们看下:
// ThreadLocalMap private Entry getEntry(ThreadLocal<?> key) { // 获取当前 ThreadLocal 的索引位置 int i = key.threadLocalHashCode & (table.length - 1); // 获取到当前位置的 Entry Entry e = table[i]; // 不等于空的话,并且等于当前的 ThreadLocal 对象 if (e != null && e.get() == key) return e; else // 什么清空走这个? 1、脏ThreadLocal 2、冲突了 return getEntryAfterMiss(key, i, e); }
// ThreadLocalMap private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { // 当前的数组 Entry[] tab = table; // 数组的长度 int len = tab.length; // 循环直到 Entry 为空了 退出循环 while (e != null) { // 获取 Entry 的 key ThreadLocal<?> k = e.get(); // 等于当前的 ThreadLocal 直接返回 if (k == key) return e; // 脏了就清除掉继续循环 if (k == null) expungeStaleEntry(i); else // 下一个索引位置继续 i = nextIndex(i, len); e = tab[i]; } return null; }
3.4 remove 方法
我们最后再来看一个remove方法:
// ThreadLocal public void remove() { // 获取到当前线程的 ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); // 不为空的话 调用ThreadLocalMap 的 remove if (m != null) m.remove(this); }
可以看到调用了 ThreadLocalMap 的 remove 方法,那么我们进去看看:
3.4.1 ThreadLocalMap 的 remove 方法
// ThreadLocalMap private void remove(ThreadLocal<?> key) { // 当前的数组 Entry[] tab = table; // 数组长度 int len = tab.length; // 获取当前 ThreadLocal 对应的索引位置 int i = key.threadLocalHashCode & (len-1); /** * 这里为什么要循环呢? * 因为取余会得到的可能不是当前 ThreadLocal 的 * 因为会冲突 冲突后会往后存放,所以循环知道找到和当前的 ThreadLocal相等的 */ for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { // reference 的 弱引用置空 e.clear(); // 清理掉当前的 ThreadLocal expungeStaleEntry(i); return; } } }
remove方法比较简单,回收掉当前的 ThreadLocal占用的 Entry,其实也会进行其它 脏ThreadLocal的清理。
4 思考
4.1 大致的引用关系图
这里简单画了一下各个类型之间的引用关系,方便大家理解哈:
4.2 为什么重新扩容或者放置新的 ThreadLocal的时候都不加锁呢?也就是ThreadLocal为什么是无锁的呢?
你想从始至终我们的 ThreadLocalMap都是依附在当前线程中的,这些东西都是你自己的东西,没别的线程跟你抢,相当于 ThreadLocal对象都是对每个线程的一个副本,自己用自己的。
4.3 常说的 ThreadLocal的内存泄漏是怎么回事?
内存泄漏是什么意思:就是我们的垃圾回收不掉已经不用的空间,不再用到的内存,没有及时释放,就叫做内存泄漏。就会一直滞留在jvm中,导致可能会OOM,但是我们可以看到 set、get其实都会对脏的 ThreadLocal 进行 value强引用的解除,那么怎么还会有泄漏呢?难道是如果数据初始化好之后,一直不调用get、set等方法,这样Entry就一直不能回收,导致内存泄漏的么?
另一个理解Threadlocal本身的存活时间就比较长,当我们的线程池线程复用起来的话,每个线程往里边set完不主动remove的话是不是就内存泄漏了,一直得不到释放。
所以使用ThreadLocal会发生 内存泄漏的前提条件:
(1)线程长时间运行而没有被销毁。 线程池中的Thread实例很容易满足此条件。
(2)ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他 ThreadLocal实例的get、set或remove操作。
4.4 ThreadLocalMap中的 key 为什么是弱引用?
首先弱引用是当仅有弱引用(WeakReference)指向的对象,只能生存到下一次垃圾回收之前。换句话说,当GC发生时,不管内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用 指向的对象,则不会被直接回收。
由于ThreadLocalMap中Entry的 Key 使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key 所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收 之后,其Entry的Key值变为null。后续当ThreadLocal的 get 、 set 或 remove 被调用时, ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。
举个例子:
public void funcA() { //创建一个线程本地变量 ThreadLocal local = new ThreadLocal<Integer>(); //设置值 local.set(100); //获取值 local.get(); //函数末尾 }
当线程执行完funcA方法后,funcA的方法栈帧将被销毁,强引用 local 的值也就没有了,但此时线程 的ThreadLocalMap里的对应的Entry的 Key 引用还指向了 ThreadLocal 实例。若Entry的 Key 引用是 强引用, 就会导致Key引用指向的ThreadLocal实例、及其Value值都不能被GC回收,这将造成严重的内存泄露.
5 小结
本节我们浏览了一下 ThreadLocal 的机制和原理,看了其核心的具体实现,有理解不对的地方欢迎指正哈。