本文将主要结合源码讲述 ThreadLocal 的使用场景和内部结构,以及 ThreadLocalMap 的内部结构;另外在阅读文本之前只好先了解一下引用和 HashMap 的相关知识,可以参考 Reference 框架概览Reference 完全解读HashMap 相关

一、使用场景

通常情况下避免多线程问题有三种方法:

  • 不使用共享状态变量;
  • 状态变量为不可变的;
  • 访问共享变量时使用同步;

而 ThreadLocal 则是通过每个线程独享状态变量的方式,即不使用共享状态变量,来消除多线程问题的,例如:

  1. @Slf4j public class TestThreadlocal {
  2. private static ThreadLocal<String> local = ThreadLocal.withInitial(() -> "init");
  3. public static void main(String[] args) throws InterruptedException {
  4. Runnable r = new TT();
  5. new Thread(r, "thread1").start();
  6. Thread.sleep(2000);
  7. new Thread(r, "thread2").start();
  8. log.info("exit");
  9. }
  10. private static class TT implements Runnable {
  11. @Override
  12. public void run() {
  13. log.info(local.get());
  14. local.set(Thread.currentThread().getName());
  15. log.info("set local name and get: {}", local.get());
  16. }
  17. }
  18. }

// 打印:

  1. [14 19:27:39,818 INFO ] [thread1] TestThreadlocal - init
  2. [14 19:27:39,819 INFO ] [thread1] TestThreadlocal - set local name and get: thread1
  3. [14 19:27:41,818 INFO ] [main] TestThreadlocal - exit
  4. [14 19:27:41,819 INFO ] [thread2] TestThreadlocal - init
  5. [14 19:27:41,819 INFO ] [thread2] TestThreadlocal - set local name and get: thread2

可以看到线程1和线程2虽然使用的是同一个 ThreadLocal 变量,但是他们之间却没有互相影响;其原因就是每个使用 ThreadLocal 变量的线程都会在各自的线程中保存一份 独立 的副本,所以各个线程之间没有相互影响;

二、ThreadLocal 结构概述

ThreadLocal 的大体结构如图所示:

如图所示:

  • 在使用 ThreadLocal 的时候,是首先获得当前线程;
  • 然后取到线程的成员变量 ThreadLocalMap(暂时可以理解为和WeakHashMap相似,后面会详细讲到);
  • 然后以当前的 ThreadLocal 变量作为 Key,取到 Entry;
  • 最后返回 Entry 中的 value;

其源代码如下:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }
  14. ThreadLocalMap getMap(Thread t) {
  15. return t.threadLocals;
  16. }

ThreadLocalMap.Entry:

另外还需要注意这里的 Entry,

  1. static class ThreadLocalMap {
  2. static class Entry extends WeakReference<ThreadLocal<?>> {
  3. /** The value associated with this ThreadLocal. */
  4. Object value;
  5. Entry(ThreadLocal<?> k, Object v) {
  6. super(k);
  7. value = v;
  8. }
  9. }
  10. ...
  11. }
  12. Reference(T referent) {
  13. this(referent, null);
  14. }

可以看到 Entry 继承了 WeakReference,并且没有传入 ReferenceQueue;关于 Reference 的部分下面我简单介绍,具体的可以参考我上面提到了两个博客;

WeakReference 表示当传入的 referent(这里就是 ThreadLocal 自身),变成弱引用的时候(即没有强引用指向他的时候);下一次 GC 将自动回收弱引用;这里没有传入 ReferenceQueue,也就代表不能集中监测回收已弃用的 Entry,而需要再次访问到对应的位置时才能检测到,具体内容下面还有讲到,注意这也是和 WeakHashMap 最大的两个区别之一;

注意如果没有手动移除 ThreadLocal,而他有一直以强引用状态存活,就会导致 value 无法回收,至最终 OOM;所以在使用 ThreadLocal 的时候,最后一定要手动移除;

三、ThreadLocalMap 结构概述

1. set 方法

ThreadLocalMap 看名字大致可以知道是类似于 HashMap的数据结构;但是有一个重要的区别是,HashMap 使用拉链法解决哈希冲突,而 ThreadLocalMap 是使用线性探测法解决哈希冲突;具体结构如图所示:

如图所示,ThreadLocalMap 里面没有链表的结构,当使用 threadLocalHashCode & (len - 1); 定位到哈希槽时,如果该位置为空则直接插入,如果不为空则检查下一个位置,直到遇到空的哈希槽;

另外它和我们通常见到的线性探测有点区别,在插入或删除的时候,会有哈希槽的移动;

源码如下:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value); // 延迟初始化
  8. }
  9. private void set(ThreadLocal<?> key, Object value) {
  10. Entry[] tab = table;
  11. int len = tab.length;
  12. int i = key.threadLocalHashCode & (len-1); // 定位哈希槽
  13. // 如果原本的位置不为空,则依次向后查找
  14. for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
  15. ThreadLocal<?> k = e.get();
  16. // 如果 threadLocal 已经存在,则直接用新值替代旧值
  17. if (k == key) {
  18. e.value = value;
  19. return;
  20. }
  21. // 如果向后找到一个已经弃用的哈希槽,则将其替换
  22. if (k == null) {
  23. replaceStaleEntry(key, value, i);
  24. return;
  25. }
  26. }
  27. // 如果定位的哈希槽为空,则直接插入新值
  28. tab[i] = new Entry(key, value);
  29. int sz = ++size;
  30. // 最后扫描其他弃用的哈希槽,如果最终超过阈值则扩容
  31. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  32. rehash();
  33. }
  34. }
  35. private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
  36. Entry[] tab = table;
  37. int len = tab.length;
  38. Entry e;
  39. int slotToExpunge = staleSlot;
  40. // 以 staleSlot 为基础,向前查找到最前面一个弃用的哈希槽,并确立清除开始位置
  41. for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
  42. if (e.get() == null) slotToExpunge = i;
  43. // 以 staleSlot 为基础,向后查找已经存在的 ThreadLocal
  44. for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
  45. ThreadLocal<?> k = e.get();
  46. // 如果向后还有目标 ThreadLocal,则交换位置
  47. if (k == key) {
  48. e.value = value;
  49. tab[i] = tab[staleSlot];
  50. tab[staleSlot] = e;
  51. // 刚交换的位置如果等于清除开始位置,则将其指向目标位置之后
  52. if (slotToExpunge == staleSlot) slotToExpunge = i;
  53. // 从开始清除位置开始扫描全表,并清除
  54. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  55. return;
  56. }
  57. // 如果在目标位置后面未找到目标 ThreadLocal,则 staleSlot 仍然是目标位置,并将开始清除位置指向后面
  58. if (k == null && slotToExpunge == staleSlot)
  59. slotToExpunge = i;
  60. }
  61. // 在目标位置替换
  62. tab[staleSlot].value = null;
  63. tab[staleSlot] = new Entry(key, value);
  64. // 如果开始清除的位置,不是目标位置,则扫描全表并清除
  65. if (slotToExpunge != staleSlot)
  66. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  67. }

其中总体思路是:

  • 如果目标位置为空,则直接插入;
  • 如果不为空,则向后查询,看是否有目标key存在,如果存在则交换位置,并插入;
  • 另外还需要确定一个跳跃扫描全表的起始位置,必须是弃用的哈希槽,如果目标位置前面有就找最前面的,如果没有就用后面的;

2. get 方法

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }
  14. private Entry getEntry(ThreadLocal<?> key) {
  15. int i = key.threadLocalHashCode & (table.length - 1);
  16. Entry e = table[i];
  17. if (e != null && e.get() == key)
  18. return e;
  19. else
  20. return getEntryAfterMiss(key, i, e);
  21. }
  22. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  23. Entry[] tab = table;
  24. int len = tab.length;
  25. while (e != null) {
  26. ThreadLocal<?> k = e.get();
  27. if (k == key)
  28. return e;
  29. if (k == null)
  30. expungeStaleEntry(i);
  31. else
  32. i = nextIndex(i, len);
  33. e = tab[i];
  34. }
  35. return null;
  36. }

从源码里面也可以看到上面讲的逻辑:

  • 首先获取 ThreadLocalMap,如果 map 为空则初始化;也可以使用 Thread.withInitial(Supplier<? extends S> supplier);工厂方法创建以初始值的 ThreadLocal,或则直接覆盖 Thread.initialValue()方法;
  • 然后用哈希定位哈希槽,如果命中则返回,未命中则向后一次查询;
  • 如果最终未找到,则用 Thread.initialValue() 方法返回初始值;

3. remove 方法

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null) m.remove(this);
  4. }
  5. private void remove(ThreadLocal<?> key) {
  6. Entry[] tab = table;
  7. int len = tab.length;
  8. int i = key.threadLocalHashCode & (len-1);
  9. for (Entry e = tab[i];
  10. e != null;
  11. e = tab[i = nextIndex(i, len)]) {
  12. if (e.get() == key) {
  13. e.clear();
  14. expungeStaleEntry(i);
  15. return;
  16. }
  17. }
  18. }
  19. public void clear() {
  20. this.referent = null;
  21. }

移除的逻辑也可 HashMap 类似:

  • 首先查找到目标哈希槽,然后清除;
  • 注意这里的清除并非直接将 Entry 置为 null,而是先将 WeakReference 的 referent置为空,在扫描全表;其实是在模拟了 WeakReference 清除的过程,如果 ThreadLocal 变成弱引用,在访问一次 ThreadLocalMap,其清除的过程是一样的;
  • 另外注意这里清除后和 HashMap 一样,容量是不会缩小的;

4. ThreadLocal 哈希计算

  1. int index = key.threadLocalHashCode & (len-1);
  2. private final int threadLocalHashCode = nextHashCode();
  3. private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
  4. private static AtomicInteger nextHashCode = new AtomicInteger();
  5. private static final int HASH_INCREMENT = 0x61c88647;

这里哈希槽的定位仍然是使用的除留余数法,当容量是2的幂时,hash % length = hash & (length-1);但是 ThreadLocalMap 和 HashMap 有点区别的是,ThreadLocalMap 的 key 都是 ThreadLocal,如果这里使用通常意义的哈希计算方法,那肯定每个 key 都会发生哈希碰撞;所以需要用一种方法将相同的 key 区分开,并均匀的分布到 2的幂的数组中;

所以就看到了上面的计算方法,ThreadLocal 的哈希值每次增加 0x61c88647;具体原因大家可以参见源码注释,其目的就是能使 key 均匀的分布到 2的幂的数组中;

5. 清除方法

  1. cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  2. private boolean cleanSomeSlots(int i, int n) {
  3. boolean removed = false;
  4. Entry[] tab = table;
  5. int len = tab.length;
  6. do {
  7. i = nextIndex(i, len);
  8. Entry e = tab[i];
  9. if (e != null && e.get() == null) {
  10. n = len;
  11. removed = true;
  12. i = expungeStaleEntry(i);
  13. }
  14. } while ( (n >>>= 1) != 0);
  15. return removed;
  16. }
  17. private int expungeStaleEntry(int staleSlot) {
  18. Entry[] tab = table;
  19. int len = tab.length;
  20. // expunge entry at staleSlot
  21. tab[staleSlot].value = null;
  22. tab[staleSlot] = null;
  23. size--;
  24. // Rehash until we encounter null
  25. Entry e;
  26. int i;
  27. for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
  28. ThreadLocal<?> k = e.get();
  29. if (k == null) {
  30. e.value = null;
  31. tab[i] = null;
  32. size--;
  33. } else {
  34. int h = k.threadLocalHashCode & (len - 1);
  35. if (h != i) {
  36. tab[i] = null;
  37. // Unlike Knuth 6.4 Algorithm R, we must scan until
  38. // null because multiple entries could have been stale.
  39. while (tab[h] != null)
  40. h = nextIndex(h, len);
  41. tab[h] = e;
  42. }
  43. }
  44. }
  45. return i;
  46. }

expungeStaleEntry:

  • 首先清除目标位置;
  • 然后向后依次扫描,直到遇到空的哈希槽;
  • 如果遇到已弃用的哈希槽则清除,如果遇到因哈希冲突后移的 ThreadLocal,则前移;

cleanSomeSlots 则是向后偏移调用 expungeStaleEntry 方法 log(n) 次,cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); 连用就可以扫描全表清除已弃用的哈希槽;

6. 扩容方法

  1. private void rehash() {
  2. expungeStaleEntries();
  3. // Use lower threshold for doubling to avoid hysteresis
  4. if (size >= threshold - threshold / 4) resize();
  5. }
  6. private void expungeStaleEntries() {
  7. Entry[] tab = table;
  8. int len = tab.length;
  9. for (int j = 0; j < len; j++) {
  10. Entry e = tab[j];
  11. if (e != null && e.get() == null) expungeStaleEntry(j);
  12. }
  13. }
  14. private void resize() {
  15. Entry[] oldTab = table;
  16. int oldLen = oldTab.length;
  17. int newLen = oldLen * 2;
  18. Entry[] newTab = new Entry[newLen];
  19. int count = 0;
  20. for (int j = 0; j < oldLen; ++j) {
  21. Entry e = oldTab[j];
  22. if (e != null) {
  23. ThreadLocal<?> k = e.get();
  24. if (k == null) {
  25. e.value = null; // Help the GC
  26. } else {
  27. int h = k.threadLocalHashCode & (newLen - 1);
  28. while (newTab[h] != null)
  29. h = nextIndex(h, newLen);
  30. newTab[h] = e;
  31. count++;
  32. }
  33. }
  34. }
  35. setThreshold(newLen);
  36. size = count;
  37. table = newTab;
  38. }

扩容时:

  • 首先扫描全表清除已弃用的哈希槽;
  • 如果清除后仍然超过阈值,则扩容;
  • 扩容时,容量增加 1 倍(初始容量为 16,所以容量一直是2的幂),然后将旧表中的值,依次插到新表;

四、InheritableThreadLocal

InheritableThreadLocal 是可以被继承的 ThreaLocal;在 Thread 中有成员变量用来继承父类的 ThreadLocalMap ;ThreadLocal.ThreadLocalMap inheritableThreadLocals;比如:

  1. @Slf4j public class TestThreadlocal {
  2. private static InheritableThreadLocal<String> local = new InheritableThreadLocal();
  3. public static void main(String[] args) throws InterruptedException {
  4. Runnable r = new TT();
  5. local.set("parent");
  6. log.info("get: {}", local.get());
  7. Thread.sleep(1000);
  8. new Thread(r, "child").start();
  9. log.info("exit");
  10. }
  11. private static class TT implements Runnable {
  12. @Override
  13. public void run() {
  14. log.info(local.get());
  15. local.set(Thread.currentThread().getName());
  16. log.info("set local name and get: {}", local.get());
  17. }
  18. }
  19. }

// 打印:

  1. [15 10:58:29,878 INFO ] [main] TestThreadlocal - get: parent
  2. [15 10:58:30,878 INFO ] [main] TestThreadlocal - exit
  3. [15 10:58:30,878 INFO ] [child] TestThreadlocal - parent
  4. [15 10:58:30,878 INFO ] [child] TestThreadlocal - set local name and get: child

总结

  • ThreadLocal 通过线程独占的方式,也就是隔离的方式,避免了多线程问题;
  • 在使用 ThreadLocal 的时候一定要手动移除,以避免内存泄漏;

并发系列(2)之 ThreadLocal 详解的更多相关文章

  1. 【Java并发系列03】ThreadLocal详解

    img { border: solid 1px } 一.前言 ThreadLocal这个对象就是为多线程而生的,没有了多线程ThreadLocal就没有存在的必要了.可以将任何你想在每个线程独享的对象 ...

  2. 高并发架构系列:Redis并发竞争key的解决方案详解

    https://blog.csdn.net/ChenRui_yz/article/details/85096418 https://blog.csdn.net/ChenRui_yz/article/l ...

  3. 深入解析ThreadLocal 详解、实现原理、使用场景方法以及内存泄漏防范 多线程中篇(十七)

    简介 从名称看,ThreadLocal 也就是thread和local的组合,也就是一个thread有一个local的变量副本 ThreadLocal提供了线程的本地副本,也就是说每个线程将会拥有一个 ...

  4. C++11 并发指南三(std::mutex 详解)

    上一篇<C++11 并发指南二(std::thread 详解)>中主要讲到了 std::thread 的一些用法,并给出了两个小例子,本文将介绍 std::mutex 的用法. Mutex ...

  5. ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借

    ASP.NET MVC深入浅出系列(持续更新)   一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...

  6. C++11 并发指南三(std::mutex 详解)(转)

    转自:http://www.cnblogs.com/haippy/p/3237213.html 上一篇<C++11 并发指南二(std::thread 详解)>中主要讲到了 std::th ...

  7. 【C/C++开发】C++11 并发指南三(std::mutex 详解)

    本系列文章主要介绍 C++11 并发编程,计划分为 9 章介绍 C++11 的并发和多线程编程,分别如下: C++11 并发指南一(C++11 多线程初探)(本章计划 1-2 篇,已完成 1 篇) C ...

  8. 分布式-技术专区-Redis并发竞争key的解决方案详解

    Redis缓存的高性能有目共睹,应用的场景也是非常广泛,但是在高并发的场景下,也会出现问题:缓存击穿.缓存雪崩.缓存和数据一致性,以及今天要谈到的缓存并发竞争.这里的并发指的是多个redis的clie ...

  9. C++11 并发指南六(atomic 类型详解四 C 风格原子操作介绍)

    前面三篇文章<C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)>.<C++11 并发指南六( <atomic> 类型详解二 std::at ...

  10. C++11 并发指南六(atomic 类型详解三 std::atomic (续))

    C++11 并发指南六( <atomic> 类型详解二 std::atomic ) 介绍了基本的原子类型 std::atomic 的用法,本节我会给大家介绍C++11 标准库中的 std: ...

随机推荐

  1. bzoj5253 [2018多省省队联测]制胡窜

    后缀自动机挺好毒瘤的题. 我们考虑哪些切点是不合法的.肯定是所有的匹配串都被切了. 我们考虑第一个切口的位置. 当第一个切口在第一个出现位置前时,第二个切口必须切掉所有的串. 当第一个切口在$l_{i ...

  2. 【Unity游戏开发】Lua中的os.date和os.time函数

    一.简介 最近马三在工作中经常使用到了lua 中的 os.date( ) 和 os.time( )函数,不过使用的时候都是不得其解,一般都是看项目里面怎么用,然后我就模仿写一下.今天正好稍微有点空闲时 ...

  3. 【Canal源码分析】Sink及Store工作过程

    一.序列图 二.源码分析 2.1 Sink Sink阶段所做的事情,就是根据一定的规则,对binlog数据进行一定的过滤.我们之前跟踪过parser过程的代码,发现在parser完成后,会把数据放到一 ...

  4. Java开源生鲜电商平台-优惠券设计与架构(源码可下载)

    Java开源生鲜电商平台-优惠券设计与架构(源码可下载) 说明:现在电商白热化的程度,无论是生鲜电商还是其他的电商等等,都会有促销的这个体系,目的就是增加订单量与知名度等等 那么对于Java开源生鲜电 ...

  5. Spark学习之Spark Streaming

    一.简介 许多应用需要即时处理收到的数据,例如用来实时追踪页面访问统计的应用.训练机器学习模型的应用,还有自动检测异常的应用.Spark Streaming 是 Spark 为这些应用而设计的模型.它 ...

  6. FOFA爬虫大法——API的简单利用

    FOFA是一款网络空间搜索引擎,它通过进行网络空间测绘,帮助研究人员或者企业迅速进行网络资产匹配,例如进行漏洞影响范围分析.应用分布统计.应用流行度等. 何为API?如果你在百度百科上搜索,你会得到如 ...

  7. SpaceSyntax【空间句法】之DepthMapX学习:第二篇 输出了什么东西 与 核心概念

    这节比较枯燥,都是原理,不过也有干货.这篇能不能听懂,就决定是否入门...所以,加油吧 博客园/B站/知乎/CSDN  @秋意正寒 转载请在文头注明本文地址 本篇讲空间句法的几个核心概念,有一些也是重 ...

  8. 【机器学习基础】熵、KL散度、交叉熵

    熵(entropy).KL 散度(Kullback-Leibler (KL) divergence)和交叉熵(cross-entropy)在机器学习的很多地方会用到.比如在决策树模型使用信息增益来选择 ...

  9. 浅析vue2.0的diff算法

    一.前言 如果不了解virtual dom,要理解diff的过程是比较困难的. 虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTe ...

  10. 使用JavaScript和D3.js实现数据可视化

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由独木桥先生发表于云+社区专栏 介绍 D3.js是一个JavaScript库.它的全称是Data-Driven Documents(数据 ...