媒介
在日常实现功能的过程当中,都会碰到想要某些 静态变量,不管是 单线程 亦大概是 多线程在使用,都不会产生相互之间的影响,也就是这个 静态变量 在线程之间是 读写隔离 的。
有一个我们常常使用的 工具类,它的并发问题就是用 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 之间的关系。
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[staleSlot].value = null; tab[staleSlot] = 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[h] != null) h = nextIndex(h, len); tab[h] = 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[i = nextIndex(i, len)]) { 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[staleSlot]; tab[staleSlot] = 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[staleSlot].value = null; tab[staleSlot] = 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[newLen]; // 记录插入的有效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[j]; 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[h] != null) h = nextIndex(h, newLen); newTab[h] = 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[i = nextIndex(i, len)]) { 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[INITIAL_CAPACITY]; // 通过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 这个功能的一整个流程,各人都有了个比较清晰的了解了。
以是通过该篇源码的分析,让我真正地意识到,我们不能光是看源码做翻译而已,我们一定要学会他们是如何思考实现这么个功能,我们要学会他们思考每一个功能的逻辑。 |