在开始看源码之前,我们必须要知道 ThreadLocal 有什么作用:ThreadLocal 使同一个变量在不同线程间隔离,即每个线程都可以有自己独立的副本,然后可以在该线程的方法间共享(随时取出使用)。不明白的话可以看文章最后一部分的使用示例。

这其实是一种空间换时间的思路,因为如果每个线程都有自己独立的副本,就不用通过加锁使线程串行化执行去保证线程安全了,节省了时间,但作为代价要为每个线程开辟一块独立的空间。

了解了 ThreadLocal 的功能后,那我们该如何设计ThreadLocal?

1.如何设计线程间隔离

首先,很容易想到每个线程都必须为 ThreadLocal 开辟一块单独的内存,但仅仅开辟一块大小等于 ThreadLocal 的内存是不够的的。因为一个线程可能有多个独立的副本,换句话说就是可以在多个类中创建 ThreadLocal,比如:

public static void main(String[] args) {new A().f1();new B().f2();
}public class A {private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();public void f1(){ threadLocal1.set("test");
}
public class B {private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();public void f2(){ threadLocal1.set(001);
}

那么对于main线程来说,test 和 001 都是它的独立副本,都要保存起来,而他俩的区别就在于保存的 ThreadLocal 实例对象不同。

PS:在我们实际开发中,TheadLocal 会常用于拦截器中,比如 LoginInterceptor 获取到 token 中保存的 userId,然后放到一个 ThreadLocal 中,后面在需要 userId 的时候就可以通过 LoginInterceptor.threadLocal.get() 就能拿到。同样的,我可能还有一个 UVInterceptor 用于获取到请求中携带的客户唯一端标识 ID,那么我就就需要再创建一个 ThreadLocal 去保存。所以说,在一个线程中,可能会有多个 TheadLocal 实例。

接下来,我们就看看在线程(Thread类)中到底是如何保存ThreadLocal的:


可以看到,每个Thread维护一个ThreadLocalMap,而存储在ThreadLocalMap内的就是一个以Entry为元素的table数组(Entry就是一个key-value结构:key为ThreadLocal,value为存储的值),所以我们可以得到以下两点信息:

  1. 数组保证了每个线程可以存储多个独立的副本
  2. Entry 提供了区分不同副本方式,即ThreadLocal实例对象不同

另外,虽然这里有两个变量,但只有 threadLocals 是直接进行set/get操作的。若在父线程中创建子线程,会拷贝父线程的 inheritableThreadLocals 到子线程。

问题:创建子线程时,子线程是得不到父线程的 ThreadLocal,有什么办法可以解决这个问题?

答:可以使用 InheritableThreadLocal 来代替 ThreadLocal,ThreadLocal 和 InheritableThreadLocal 都是线程的属性,所以可以做到线程之间的数据隔离,在多线程环境下我们经常使用,但在有子线程被创建的情况下,父线程 ThreadLocal 是无法传递给子线程的,但 InheritableThreadLocal 可以,主要是因为在线程创建的过程中,会把InheritableThreadLocal 里面的所有值传递给子线程,具体代码如下:

// 当父线程的 inheritableThreadLocals 的值不为空时
// 会把 inheritableThreadLocals 里面的值全部传递给子线程
if (parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

看源码前的要理解的逻辑终于说完了,下面进入正戏…

2.ThreadLocal

ThreadLocal 核心成员变量及主要构造函数:

// ThreadLocal使用了泛型,所以可以存放任何类型
public class ThreadLocal<T> {// 当前 ThreadLocal 的 hashCode,作用是计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置// nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);}private final int threadLocalHashCode = nextHashCode();// nextHashCode 直接决定 threadLocalHashCode(= nextHashCode++)// 这么做因为ThreadLocal可能在不同类中new出来多个,但线程只有一个,若每次下标都从同一位置开始,虽然有hash碰撞处理策略,但仍然会影响效率// static:保证了nextHashCode的唯一性,间接保证了threadHashCode唯一性private static AtomicInteger nextHashCode = new AtomicInteger();// ThreadLocalMap(核心)// 注:虽然 ThreadLocal 是它的 Entry 的 key,但是它是 ThreadLocal 的静态内部类static class ThreadLocalMap{...}// 只有空参构造public ThreadLocal() {}// 计算 ThreadLocal 的 hashCode 值,就是通过CAS让 nextHashCode++private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}//......
}

2.1 set()

拿到当前线程的 threadLocals 并将 Entry(当前ThreadLocal对象,value)放入。另外,因为 set 操作每个线程都是串行的,所以不会有线程安全的问题

public void set(T value) {// 拿到当前线程Thread t = Thread.currentThread();// 拿到当前线程的ThreadLocalMap,即threadLocals变量ThreadLocalMap map = getMap(t);// 当前 threadLocal 非空,即之前已经有独立的副本数据了if (map != null)map.set(this, value); // 直接将当前 threadLocal和value传入// 当前threadLocal为空elsecreateMap(t, value); // 初始化ThreadLocalMap
}ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

这里说一下 createMap 初始化 ThreadLocalMap,它最后会走到内部类 ThreadLocalMap 的构造函数

可以看到数组的初始大小跟 HashMap 一样,都是 16;值得注意的是,它的扩容阈值并不是 HashMap 那样等于数组容量乘扩容因子(一般为0.75),而是扩容阈值直接等于当前数组程度,即空间用完了就扩容(2倍)。

注:其实这样做合理,因为毕竟只有一个线程在使用,初始的 16 个位置已经够多了,没必要在我用 12/13 个时扩个容。

2.2 get()

在当前线程的 theadLocals 中获取当前ThreadLocal对象对应的value

  1. 在当前线程拿到threadLocals
  2. 若threadLocals=null,则将其初始化
  3. 通过当前ThreadLocal对象获取到相应Entry
    • entry != null ,返回result
    • entry = null ,返回null
public T get() {// 拿出当前线程Thread t = Thread.currentThread();// 从线程中拿到 threadLocals(ThreadLocalMap)ThreadLocalMap map = getMap(t);if (map != null) {// 从 map 中拿到相应entryThreadLocalMap.Entry e = map.getEntry(this);// 如果不为空,读取当前 ThreadLocal 中保存的值if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 否则给当前线程的 ThreadLocal 初始化,并返回初始值 nullreturn setInitialValue();
}

3.ThreadLocalMap

// 静态内部类,可直接被外部调用
static class ThreadLocalMap {// Entry(k,v)// k = WeakReference 是弱引用,当没有引用指向时,会直接被回收static class Entry extends WeakReference<ThreadLocal<?>> {// 当前 ThreadLocal 关联的值Object value;// WeakReference 的引用 referent 就是 ThreadLocalEntry(ThreadLocal<?> k, Object v) {super(k);value = v;}}// 存储 (ThreadLocal,Obj) 的数组// 这里其实采用的是哈希表,后面可以看到解决哈希冲突的办法是开放寻址法private Entry[] table;// 数组的初始化大小private static final int INITIAL_CAPACITY = 16; // 扩容的阈值,默认是数组大小的三分之二private int threshold;//.......
}

这里放一个链接吧:【数据结构】哈希表,从特性到哈希冲突再到应用,可以先看看哈希表解决冲突的几种方案,然后再看 set 方法…

3.1 set()

将 Entry(threadLocal,Object Value)放入 threadLocals的数组

  1. 获取到 threadLocals 的数组
  2. 计算当前ThreadLocal对应的数组下标
  3. 将Entry(threadLocal,Object Value)放入数组
    • 无hash碰撞,new Entry放入
    • 若出现hash碰撞,则i++,直到找到没有Entry的位置,new Entry放入(开放寻址法)
    • 若碰见key相同(ThreadLocal),则替换value
  4. 判断是否需要扩容
private void set(ThreadLocal<?> key, Object value) {// 1.拿到当前threadLocals的数组Entry[] tab = table;int len = tab.length;// 2.计算当前 ThreadLocal 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余int i = key.threadLocalHashCode & (len-1);// 可以看到循环的结束条件是 tab[i]==null,即无哈希冲突// 若出现哈希冲突时,依次向后(i++)寻找空槽点。nextIndex方法就是让在不超过数组长度的基础上,把数组的索引位置 + 1// nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);} 如果到数组末尾了,返回 0(循环)for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 找到内存地址一样的 ThreadLocal,直接替换if (k == key) {e.value = value;return;}// 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉if (k == null) {replaceStaleEntry(key, value, i);return;}}// 当前 i 位置是无值的,可以被当前 thradLocal 使用tab[i] = new Entry(key, value);int sz = ++size;// 当数组大小大于等于扩容阈值(数组的长度)时,进行扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

3.2 getEntry()

获取相应节点Entry

  1. 计算当前ThreadLocal对应的索引位置(hashcode 取模数组大小-1 )
  2. 若 e != null,返回当前Entry
  3. 若 e == null 或 有但key(ThreadLocal)不符,调用 getEntryAfterMiss 自旋进行寻找
private Entry getEntry(ThreadLocal<?> key) {// 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// e 不为空 && e 的 ThreadLocal 的内存地址和 key 相同if (e != null && e.get() == key)return e; // 直接返回// 因为上面解决Hash冲突的方法是i++,所以会出现计算出的槽点为空或者不等于当前ThreadLocal的情况elsereturn getEntryAfterMiss(key, i, e); // 继续通过 getEntryAfterMiss 方法找
}

getEntryAfterMiss:根据 thradLocalMap set 时解决数组索引位置冲突的逻辑,该方法的寻找逻辑也是对应的,即自旋 i+1,直到找到为止

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的while (e != null) {ThreadLocal<?> k = e.get();// 内存地址一样,表示找到了if (k == key)return e;// 删除没用的 keyif (k == null)expungeStaleEntry(i);// 继续使索引位置 + 1elsei = nextIndex(i, len);e = tab[i];}return null;
}

3.3 resize()

ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了

  1. 拿到 threadLocals 的table
  2. 初始化新数组,大小为原来2倍
  3. 将老数组拷贝到新数组
    • 根据key(ThreadLocal)计算新的索引位置
    • 若出现hash碰撞,i++
  4. 计算新的扩容阈值,将新数组赋给table

注:由于是一个线程,所以所有操作都是串行的,所以不存在线程安全问题。

private void resize() {// 1.拿出旧的数组Entry[] oldTab = table;int oldLen = oldTab.length;// 2.计算新数组的大小,为老数组的两倍int newLen = oldLen * 2;// 初始化新数组Entry[] newTab = new Entry[newLen];int count = 0;// 3.老数组的值拷贝到新数组上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 {// 计算 ThreadLocal 在新数组中的位置int h = k.threadLocalHashCode & (newLen - 1);// 如果出现哈希冲突,即索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置while (newTab[h] != null)h = nextIndex(h, newLen);// 给新数组赋值newTab[h] = e;count++;}}}// 4.计算新数组下次扩容阈值,为数组长度的长度setThreshold(newLen);size = count;table = newTab;
}

4.使用示例

下面启动 9 个子线程,每个子线程都将名字(thread - i)存在同一个变量中,然后再打印出来。可以想到,如果同一个变量可以做到线程间隔离(互补影响),控制台正确的结果应该是 thread - 0 到 thread - 8。

下面就分别演示演示这个变量的两种实现:1.普通变量String ,2.ThreadLocal<String>

4.1 普通变量

public class StringTest {// 保存线程名的普通变量valueprivate String value;// 不直接设置value,而是暴露出get和set方法private String getString() { return string; }private void setString(String string) { this.string = string; }public static void main(String[] args) {StringTest test= new StringTest ();int threads = 9; // 要启动的线程个数CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束// 启动9个子线程for (int i = 0; i < threads; i++) {Thread thread = new Thread(() -> {test.setString(Thread.currentThread().getName()); // 向变量value中存入线程名 thread - iSystem.out.println(test.getString()); // 然后打印出来。注:这里可能存在并发countDownLatch.countDown(); // 门栓-1}, "thread - " + i); thread.start();}}countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行
}

结果如下:

thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

可以看到没有 thread - 0,反而 thread - 1 出现了两次,所以使用普通类型的变量无法实现同一变量对于不同线程隔离

4.2 ThreadLocal

使用ThreadLocal时,一般声明为static,原因有二:

public class ThreadLocalStringTest {// 保存线程名的ThreadLocal变量threadLocal// 注:这里除了是String,也可是别的任何类型(Integer,List,Map...)private static ThreadLocal<String> threadLocal = new ThreadLocal<>();// 不直接操作 threadLocal,而是封装成 set/get 方法private String getString() { return threadLocal.get(); }private void setString(String string) { threadLocal.set(string);}public static void main(String[] args) {ThreadLocalStringTest test= new ThreadLocalStringTest();int threads = 9; // 要创建的子线程个数CountDownLatch countDownLatch = new CountDownLatch(threads); // countDownLatch 用于防止主线程在子线程未完成前结束// 创建 9 个线程for (int i = 0; i < threads; i++) {Thread thread = new Thread(() -> {test.setString(Thread.currentThread().getName()); // 向ThreadLocal中存入当前线程名 thread - iSystem.out.println(test.getString()); // 向ThreadLocal获取刚存的线程名。注:可能存在并发countDownLatch.countDown(); // 门栓-1}, "thread - " + i);thread.start();}countDownLatch.await(); // 等countDownLatch为0时,主线程恢复运行}}

运行结果:

thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

ThreadLocal 源码深析及使用示例相关推荐

  1. ThreadLocal源码剖析

    目录 一.ThreadLocal 1.1源码注释 1.2 源码剖析 散列算法-魔数0x61c88647 set操作 get操作 remove操作 1.3 功能测试 1.4 应用场景 二.变量可继承的T ...

  2. spring源码刨析总结

    spring源码刨析笔记 1.概述 spring就是 spring Framework Ioc Inversion of Control(控制反转/反转控制) DI Dependancy Inject ...

  3. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  4. 老李推荐:第3章3节《MonkeyRunner源码剖析》脚本编写示例: MonkeyImage API使用示例 1...

    老李推荐:第3章3节<MonkeyRunner源码剖析>脚本编写示例: MonkeyImage API使用示例 在上一节的第一个"增加日记"的示例中,我们并没有看到日记 ...

  5. Java并发编程之ThreadLocal源码分析

    1 一句话概括ThreadLocal   什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本. 2 ThreadLocal使用场景   用一句话总结 ...

  6. Java8 ThreadLocal 源码分析

    可参考文章: Java8 IdentityhashMap 源码分析 IdentityhashMap 与 ThreadLocalMap 一样都是采用线性探测法解决哈希冲突,有兴趣的可以先了解下 Iden ...

  7. springMvc源码刨析笔记

    springMvc源码刨析笔记 MVC 全名是 Model View Controller,是 模型(model)-视图(view)-控制器(controller) 的缩写, 是⼀种⽤于设计创建 We ...

  8. zookeeper笔记+源码刨析

    会不断更新!冲冲冲!跳转连接 https://blog.csdn.net/qq_35349982/category_10317485.html zookeeper 1.介绍 Zookeeper 分布式 ...

  9. django源码简析——后台程序入口

    django源码简析--后台程序入口 这一年一直在用云笔记,平时记录一些tips或者问题很方便,所以也就不再用博客进行记录,还是想把最近学习到的一些东西和大家作以分享,也能够对自己做一个总结.工作中主 ...

最新文章

  1. laravel It is unsafe to run Dusk in production. In DuskServiceProvider.php line 43:错误处理
  2. python游戏服务器框架_Scut游戏服务器免费开源框架--快速开发(2)
  3. BLE 数据包格式解析
  4. c++对象模型-虚拟析构函数
  5. 【项目管理】Scrum内容整理
  6. 基于glibc的程序在android上的移植
  7. 解决JS浮点数(小数)计算加减乘除的BUG
  8. Android 系统(36)---Android O、N版本修改dex2oat编译选项
  9. 计算机信息安全技术计算题,计算机信息安全技术练习题.doc
  10. Java基础面试题:常见的异常类有哪些?
  11. 查找算法——插值查找
  12. 普林斯顿微积分读本05第四章--求解多项式的极限问题
  13. 软件质量与测试的新纪元
  14. 如何将python转换成exe执行
  15. 计算机关机后风扇还转,电脑关机后cpu风扇还在转怎么办?解决电脑关机后cpu散热器还在转...
  16. python爬取豆瓣图书前250
  17. 嘉应学院计算机专业毕业好找工作吗,嘉应学院毕业证两字之差致学生求职碰壁...
  18. 在线2进制8进制10进制16进制进制转换工具
  19. 如何查看电脑有几个内存条插槽
  20. no source Theme.AppCompat.Light的解决方法

热门文章

  1. 聊聊程序员35岁危机
  2. 电脑系统重装后音频驱动程序怎么修复
  3. matlab图像对折,Matlab下如何将一个索引图像进行对折小程序--原创
  4. 在IDEA中手动创建基于Maven的Servlet项目
  5. Surface Book2 购买、使用、体验
  6. 对前端的一些粗浅的认识
  7. 二次元博客系统Halo
  8. 文件夹里的文件怎么批量打印呢?
  9. 小米实习面试总结(1)
  10. 智慧工厂用到的技术_智慧工厂如何实现智能化