详解ThreadLocal和ThreadLocalMap源码
媒介在日常实现功能的过程当中,都会碰到想要某些 静态变量,不管是 单线程 亦大概是 多线程在使用,都不会产生相互之间的影响,也就是这个 静态变量 在线程之间是 读写隔离 的。
有一个我们常常使用的 工具类,它的并发问题就是用 ThreadLocal来解决的,我相信大多数人都看过,那就是 SimpleDateFormat 日期格式化的工具类的 多线程安全问题,各人去网上搜的话,应该会有一堆人都说使用 ThreadLocal。
正文
ThreadLocal的定义
ThreadLocal 的表意是 线程本地 的意思,用来存放可以或许 线程隔离 的变量,那就是 线程本地变量。也就是说,当我们把变量保存在ThreadLocal当中时,就可以或许实现这个变量的 线程隔离。
ThreadLocal的使用示例
我们先来看两个例子,这里也刚好涉及到两个概念,分别是值通报和引用通报。
[*]值通报
public class ThreadLocalTest { private static ThreadLocal threadLocal = new ThreadLocal(){ protected Integer initialValue(){ return 0; } }; // 值通报 @Test public void testValue(){ for (int i = 0; i < 5; i++) { new Thread(() -> { Integer temp = threadLocal.get(); threadLocal.set(temp + 5); System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal.get()); }, "thread-" + i).start(); } }}以上程序的输出效果是:
current thread is thread-1 num is 5current thread is thread-3 num is 5current thread is thread-0 num is 5current thread is thread-4 num is 5current thread is thread-2 num is 5我们可以看到,每一个线程打印出来的都是5,哪怕我是先通过 ThreadLocal.get()方法获取变量,然后再 set进去,依然不会进行重复叠加。这就是线程隔离。
但是对于引用通报来说,我们又必要多留意一下了,直接上例子看看。
[*]引用通报
public class ThreadLocalTest { static NumIndex numIndex = new NumIndex(); private static ThreadLocal threadLocal1 = new ThreadLocal() { protected NumIndex initialValue() { return numIndex; } }; static class NumIndex { int num = 0; public void increment() { num++; } } // 引用通报 @Test public void testReference() { for (int i = 0; i < 5; i++){ new Thread(() -> { NumIndex index = threadLocal1.get(); index.increment(); threadLocal1.set(index); System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal1.get().num); }, "thread-" + i).start(); } }}我们看看运行的效果
current thread is thread-0 num is 2current thread is thread-2 num is 3current thread is thread-1 num is 2current thread is thread-4 num is 5current thread is thread-3 num is 4我们看到值不但没有被隔离,而且还出现了线程安全的问题。
以是我们一定要留意值通报和引用通报的区别,在这里也不讲这两个概念了。
源码分析
想要更加深入地了解 ThreadLocal 的作用,就必要检察它的源码实现,整个类加起来也不过七八百行而已。分为 ThreadLocal 和 ThreadLocalMap 两部门分析它的源码分析。下面是 ThreadLocal 和 ThreadLocalMap 以及 Thread 之间的关系。
https://p26.toutiaoimg.com/large/pgc-image/dca11cb1945a43f5a6ea2bc6febc67f1
ThreadLocalMap源码分析
ThreadLocalMap 在 Thread 类中作为一个成员属性而被引用。
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null;}ThreadLocalMap 是 ThreadLocal 内里的一个 静态内部类,但是确实一个很关键的东西。
我盼望通过我的文章,不求可以或许带给你什么牛逼的技术,但是至少能让你明白,我们必要学习的是这些大牛的严谨的思维逻辑。
言归正传, ThreadLocalMap究竟是什么?我们要这么想,既然是线程本地变量,而且我们可以通过get和set方法可以或许获取和赋值。
[*]那我们赋值的内容,究竟保存在什么结构当中?
[*]它究竟是怎么做到线程隔离的?
[*]当我 get 和 set 的时候,它究竟是怎么做到 线程-value 的对应关系进行保存的?
ThreadLocalMap 是 ThreadLocal 非常核心的内容,是维护我们 线程 与 变量之间 关系的一个类,看到是 Map结尾,那我们也可以或许知道它现实上就是一个键值对。至于 Key 是什么,我们会在源码分析当中看出来。
Entry内部类
以下源码都是抽取讲解部门的内容来展示
static class ThreadLocalMap { /** * 自定义一个Entry类,并继承自弱引用 * 用来保存ThreadLocal和Value之间的对应关系 * * 之以是用弱引用,是为了解决线程与ThreadLocal之间的强绑定关系 * 会导致假如线程没有被回收,则GC便一直无法回收这部门内容 */ static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; }}我相信有人会有疑问,假如在我要用的时候,被回收了怎么办?下面的代码会一步步地让你明白,你思量到的问题,这些大牛都已经想到并且解决了。接着往下学吧!
getEntry和getEntryAfterMiss方法
通过方法名我们就能看得出是从 ThreadLocal 对应的 ThreadLocalMap 当中获取 Entry节点,在这我们就要思考了。
[*]我们要通过什么获取对应的 Entry。
[*]我们通过上面知道使用了 弱引用,假如被 GC 回收了没有获取到怎么办?
[*]不在通过计算得到的下标上,又要怎么办?
[*]假如 ThreadLocal对应的 ThreadLocalMap不存在要怎么办?
以上这4个问题是我自己在看源码的时候可以或许想到的东西,有些问题的答案光看 ThreadLocalMap 的源码是看不出以是然的,必要结合之后的 ThreadLocal 源码分析。
/** * 获取ThreadLocal的索引位置,通过下标索引获取内容 */private Entry getEntry(ThreadLocal key) { // 通过hashcode确定下标 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table; // 假如找到则直接返回 if (e != null && e.get() == key) return e; else // 找不到的话接着从i位置开始向后遍历,基于线性探测法,是有可能在i之后的位置找到的 return getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; // 循环向后遍历 while (e != null) { // 获取节点对应的k ThreadLocal k = e.get(); // 相等则返回 if (k == key) return e; // 假如为null,触发一次一连段清理 if (k == null) expungeStaleEntry(i); // 获取下一个下标接着进行判断 else i = nextIndex(i, len); e = tab; } return null;}一看这两个方法名,我们就知道这两个方法就是获取Entry节点的方法。
我们起首看 getEntry(ThreadLocalkey) 和 getEntryAfterMiss(ThreadLocalkey,inti,Entrye) 这个方法就看出来了,直接根据 ThreadLocal 对象来获取,以是我们可以再次证明, key 就是 ThreadLocal 对象,我们来看看它的流程:
[*]先根据 key 的 hashcode&table.length-1 来确定在 table 当中的下标。
[*]假如获取到直接返回,没获取到的话,就接着往后遍历看是否能获取到(因为用的是线性探测法,往后遍历有可能获取到效果)。
[*]进入了 getEntryAfterMiss 方法进行 线性探测,假如获取到则直接返回;获取的 key为 null,则触发一次 一连段清理(现实上在许多方法当中都会触发该方法,常常会进行一连段清理,这是 ThreadLocal 核心的清理方法),进行 垃圾回收。
expungeStaleEntry方法
这可以说是 ThreadLocal 非常核心的一个清理方法,为什么会必要清理呢?或许许多人想不明白,我们用 List 大概是 Map 也好,都没有说要清理内里的内容。
对于线程来说,隔离的 本地变量,并且使用的是 弱引用,那便有可能在 GC 的时候就被回收了。
[*]假如有许多 Entry 节点已经被回收了,但是在 table 数组中还留着位置,这时候不清理就会浪费资源
[*]在清理节点的同时,可以将后续非空的 Entry 节点重新计算下标进行排放,这样子在 get 的时候就能快速定位资源,加速效率。
/** * 这个函数可以看做是ThreadLocal里的核心清理函数,它主要做的事情就是 * 1、从staleSlot开始,向后遍历将ThreadLocal对象被回收所在Entry节点的value和Entry节点本身设置null,方便GC,并且size自减1 * 2、并且会对非null的Entry节点进行rehash,只要不是在当前位置,就会将Entry挪到下一个为null的位置上 * 以是现实上是对从staleSlot开始做一个一连段的清理和rehash操作 */private int expungeStaleEntry(int staleSlot) { // 新的引用指向table Entry[] tab = table; // 获取长度 int len = tab.length; // expunge entry at staleSlot // 先将传过来的下标置null tab.value = null; tab = null; // table的size-1 size--; // Rehash until we encounter null Entry e; int i; // 遍历删除指定节点全部后续节点当中,ThreadLocal被回收的节点 for (i = nextIndex(staleSlot, len); (e = tab) != null; i = nextIndex(i, len)) { // 获取entry当中的key ThreadLocal k = e.get(); // 假如ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC // 并且size-1 if (k == null) { e.value = null; tab = null; size--; } else { // 假如不为null // 重新计算key的下标 int h = k.threadLocalHashCode & (len - 1); // 假如是当前位置则遍历下一个 // 不是当前位置,则重新从i开始找到下一个为null的坐标进行赋值 if (h != i) { tab = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab != null) h = nextIndex(h, len); tab = e; } } } return i;}上面的代码注释我相信已经是写的很清晰了,这个方法现实上就是从 staleSlot 开始做一个一连段的清理和 rehash 操作。
set方法系列
接下来我们看看 set 方法,自然就是要将我们的变量保存进 ThreadLocal 当中,现实上就是保存到 ThreadLocalMap 当中去,在这里我们一样要思考几个问题。
[*]假如该 ThreadLocal 对应的 ThreadLocalMap 还不存在,要怎么处置处罚?
[*]假如所计算的下标,在 table 当中已经存在 Entry 节点了怎么办?
我想通过上面部门代码的讲解,对这两个问题,各人也都比较有思路了吧。
/** * ThreadLocalMap的set方法,这个方法还是挺关键的 * 通过这个方法,我们可以看出该哈希表是用线性探测法来解决冲突的 */private void set(ThreadLocal key, Object value) { // 新开一个引用指向table Entry[] tab = table; // 获取table的长度 int len = tab.length; // 获取对应ThreadLocal在table当中的下标 int i = key.threadLocalHashCode & (len-1); /** * 从该下标开始循环遍历 * 1、如遇雷同key,则直接替换value * 2、假如该key已经被回收失效,则替换该失效的key */ for (Entry e = tab; e != null; e = tab) { ThreadLocal k = e.get(); if (k == key) { e.value = value; return; } // 假如 k 为null,则替换当前失效的k所在Entry节点 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 找到空的位置,创建Entry对象并插入 tab = new Entry(key, value); // table内元素size自增 int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { // 新开一个引用指向table Entry[] tab = table; // 获取table的长度 int len = tab.length; Entry e; // 记录当前失效的节点下标 int slotToExpunge = staleSlot; /** * 通过这个for循环的prevIndex(staleSlot, len)可以看出 * 这是由staleSlot下标开始向前扫描 * 查找并记录最前位置value为null的下标 */ for (int i = prevIndex(staleSlot, len); (e = tab) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; /** * 通过for循环nextIndex(staleSlot, len)可以看出 * 这是由staleSlot下标开始向后扫描 */ for (int i = nextIndex(staleSlot, len); (e = tab) != null; i = nextIndex(i, len)) { // 获取Entry节点对应的ThreadLocal对象 ThreadLocal k = e.get(); /** * 假如与新的key对应,直接赋值value * 则直接替换i与staleSlot两个下标 */ if (k == key) { e.value = value; tab = tab; tab = e; // Start expunge at preceding stale entry if it exists // 通过注释看出,i之前的节点里,没有value为null的环境 if (slotToExpunge == staleSlot) slotToExpunge = i; /** * 在调用cleanSomeSlots进行启发式清理之前 * 会先调用expungeStaleEntry方法从slotToExpunge到table下标所在为null的一连段进行一次清理 * 返回值便是table[]为null的下标 * 然后以该下标--len进行一次启发式清理 * 终极内里的方法现实上还是调用了expungeStaleEntry * 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函数 */ cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } /** * 假如当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点 * 则slotToExpunge赋值为当前位置 */ if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot // 假如并没有在table当中找到该key,则直接在当前位置new一个Entry tab.value = null; tab = new Entry(key, value); /** * 在上面的for循环探测过程当中 * 假如发现任何无效的Entry节点,则slotToExpunge会被重新赋值 * 就会触发一连段清理和启发式清理 */ if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}/** * 启发式地清理被回收的Entry * i对应的Entry是非无效的,有可能是失效被回收了,也有可能是null * 会有两个地方调用到这个方法 * 1、set方法,在判断是否必要resize之前,会清理并rehash一遍 * 2、替换失效的节点时候,也会进行一次清理 */private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab; // Entry对象不为空,但是ThreadLocal这个key已经为null if (e != null && e.get() == null) { n = len; removed = true; /** * 调用该方法进行回收 * 现实上不是只回收 i 这一个节点而已 * 而是对 i 开始到table所在下标为null的范围内,对那些节点都进行一次清理和rehash */ i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed;}/** * 对table进行扩容,因为要保证table的长度是2的幂,以是扩容就扩大2倍 */private void resize() { // 获取旧table的长度,并且创建一个长度为旧长度2倍的Entry数组 Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry; // 记录插入的有效Entry节点数 int count = 0; /** * 从下标0开始,逐个向后遍历插入到新的table当中 * 1、如碰到key已经为null,则value设置null,方便GC回收 * 2、通过hashcode & len - 1计算下标,假如该位置已经有Entry数组,则通过线性探测向后探测插入 */ for (int j = 0; j < oldLen; ++j) { Entry e = oldTab; if (e != null) { ThreadLocal k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab != null) h = nextIndex(h, newLen); newTab = e; count++; } } } // 重新设置扩容的阈值 setThreshold(newLen); // 更新size size = count; // 指向新的Entry数组 table = newTab;}以上的代码就是调用 set 方法往 ThreadLocalMap 当中保存 K-V 关系的一系列代码,我就不分开再一个个讲了,这样各人看起来估计也比较方便。
我们可以来看看的 set 方法的整个实现流程:
[*]先通过 hashcode&(len-1)来定位该 ThreadLocal 在 table 当中的下标
[*]for循环向后遍历。假如获取 Entry 节点的 key 与我们必要操作的 ThreadLocal 相等,则直接替换 value。假如遍历的时候拿到了 key 为 null 的环境,则调用 replaceStaleEntry 方法进行与之替换。
[*]假如上述两个环境都是,则直接在计算的出来的下标当中 new 一个 Entry 阶段插入。
[*]进行一次 启发式地清理 并且假如插入节点后的 size 大于扩容的阈值,则调用 resize方法进行扩容。
remove方法
既然是Map情势进行存储,我们有put方法,那肯定就会有remove的时候,任何一种数据结构,肯定都得符合增编削查的。
我们直接来看看代码。
/** * Remove the entry for key. * 将ThreadLocal对象对应的Entry节点从table当中删除 */private void remove(ThreadLocal key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab; e != null; e = tab) { if (e.get() == key) { // 将引用设置null,方便GC e.clear(); // 从该位置开始进行一次一连段清理 expungeStaleEntry(i); return; } }}我们可以看到,remove节点的时候,也会使用线性探测的方式,当找到对应key的时候,就会调用clear将引用指向null,并且会触发一次一连段清理。
我相信通过以上对 ThreadLocalMap的源码分析,已经让各人对其有了个基本的概念认识,相信对各人明白ThreadLocal这个概念的时候,已经不是停顿在知道它就是为了实现线程本地变量而已了。
接下来我们来看看 ThreadLocal 的源码分析。
ThreadLocal源码分析
ThreadLocal的源码相对于来说就简朴许多了,因为主要都是ThreadLocalMap这个内部类在干活,在管理我们的本地变量。
get方法系列
/** * 获取当前线程本地变量的值 */public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 假如map不为空 if (map != null) { // 假如当前ThreadLocal对象对应的Entry还存在 ThreadLocalMap.Entry e = map.getEntry(this); // 并且Entry不为null,返回对应的值,否则都执行setInitialValue方法 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 假如该线程对应的ThreadLocalMap还不存在,则执行初始化方法 return setInitialValue();}ThreadLocalMap getMap(Thread t) { return t.threadLocals;}private T setInitialValue() { // 获取初始值,一样平常是子类重写 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 假如map不为null if (map != null) // 调用ThreadLocalMap的set方法进行赋值 map.set(this, value); // 否则创建个ThreadLocalMap进行赋值 else createMap(t, value); return value;}/** * 构造参数创建一个ThreadLocalMap代码 * ThreadLocal为key,我们的泛型为value */ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 初始化table的大小为16 table = new Entry; // 通过hashcode & (长度-1)的位运算,确定键值对的位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 创建一个新节点保存在table当中 table = new Entry(firstKey, firstValue); // 设置table内元素为1 size = 1; // 设置扩容阈值 setThreshold(INITIAL_CAPACITY);}ThreadLocal 的 get 方法也不难,就几行代码,但是当它结合了 ThreadLocalMap 的方法后,这整个逻辑就值得我们深入研究写这个工具的人的思维了。
我们来看看它的一个流程吧:
1. 获取当前线程,根据当前线程获取对应的 ThreadLocalMap。
2. 在 ThreadLocalMap 当中获取该 ThreadLocal 对象对应的 Entry 节点,并且返回对应的值。
3. 假如获取到的 ThreadLocalMap 为 null,则证明还没有初始化,就调用 setInitialValue 方法。
[*]在调用 setInitialValue 方法的时候,会使用双重保证,再进行获取一次。
[*]ThreadLocalMap 假如依然为 null,就终极调用 ThreadLocalMap 的构造方法。
set方法系列
在这里我也不对 ThreadLocal的set方法做太多介绍了,结合上面的 ThreadLocalMap的set方法,我想就可以对上面每个方法思考出的问题有个大概的答案。
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取线程所对应的ThreadLocalMap,从这可以看出每个线程都是独立的 ThreadLocalMap map = getMap(t); // 假如map不为空,则k-v赋值,看出k是this,也就是当前ThreaLocal对象 if (map != null) map.set(this, value); // 假如获取的map为空,则创建一个并保存k-v关系 else createMap(t, value);}其实 ThreadLocal的set方法很简朴的,最主要的都是调用了 ThreadLocalMap的set方法,内里才是真正核心的执行流程。
不过我们照样来看看这个流程:
[*]获取当前线程,根据当前线程获取对应的 ThreadLocalMap
[*]假如对应的 ThreadLocalMap不为null,则调用其的set方法保存对应关系
[*]假如map为null,就终极调用 ThreadLocalMap的构造方法创建一个 ThreadLocalMap并保存对应关系
总结
上面通过对 ThreadLocal 和 ThreadLocalMap 两个类的源码进行了分析,我想对于ThreadLocal 这个功能的一整个流程,各人都有了个比较清晰的了解了。
以是通过该篇源码的分析,让我真正地意识到,我们不能光是看源码做翻译而已,我们一定要学会他们是如何思考实现这么个功能,我们要学会他们思考每一个功能的逻辑。
页:
[1]