精通高并发与多线程,却不会用ThreadLocal?( 二 )


侧重点: 多个线程之间同步访问资源

  • ThreadLocal
原理: ThreadLocal 采用 "以空间换时间" 的方式 , 为每个线程都提供了一份变量的副本 , 从而实现同时访问而互不干扰
侧重点: 多线程中让每个线程之间的数据相互隔离
3. 内部结构从上面的案例中我们可以看到 ThreadLocal 的两个主要方法分别是 set() 和 get()
那我们不妨猜想一下 , 如果让我们来设计 ThreadLocal, 我们该如何设计 , 是否会有这样的想法:每个 ThreadLocal 都创建一个 Map , 然后用线程作为 Map 的 key , 要存储的局部变量作为 Map 的 value, 这样就能达到各个线程的局部变量隔离的效果 。
精通高并发与多线程,却不会用ThreadLocal?文章插图
这个想法也是没错的 , 早期的 ThreadLocal 便是这样设计的 , 但是在 JDK 8 之后便更改了设计 , 如下:
精通高并发与多线程,却不会用ThreadLocal?文章插图
设计过程:
  1. 每个 Thread 线程内部都有一个 ThreadLocalMap
  2. ThreadLocalMap 中存储着以 ThreadLocal 对象为 key, 线程变量为 value
  3. Thread 内部的 Map 是由 ThreadLocal 维护的 , 由 ThreadLocal 负责向 Map 设置和获取线程的变量值
  4. 对于不同的线程 , 每次获取副本值时 , 别的线程并不能获取到线程的副本值 , 这样就会形成副本的隔离 , 互不干扰
注: 每个线程都要有自己的一个 map , 但是这个类就是一个普通的 Java 类 , 并没有实现 Map 接口 , 但是具有类似 Map 类似的功能 。
精通高并发与多线程,却不会用ThreadLocal?文章插图
通过这样实现看起来貌似会比之前我们猜想得更加复杂 , 这样做的好处是什么呢?
  • 每个 Map 存储的 Entry 数量就会变少 , 因为之前的存储数量由 Thread 的数量决定 , 现在是由 ThreadMap 的数量决定 , 在实际开发中 , ThreadLocal 的数量要更少于 Thread 的数量 。
  • 当 Thread 销毁之后 , 对应的 ThreadLocalMap 也会随之销毁 , 能减少内存的使用
4. 源码分析
精通高并发与多线程,却不会用ThreadLocal?文章插图
首先我们先看 ThreadLocalMap 中有哪些成员:
精通高并发与多线程,却不会用ThreadLocal?文章插图
如果你看过 HashMap 的源码 , 肯定会觉得这几个特别熟悉 , 其中:
  • INITIAL_CAPACITY:初始容量 , 必须是 2 的整次幂
  • table:存放数据的table
  • size:数组中 entries 的个数 , 用于判断 table 当前使用量是否超过阈值
  • threshold:进行扩容的阈值 , 表使用量大于它的时候会进行扩容
ThreadLocalsThread 类中有个类型为 ThreadLocal.ThreadLocalMap 类型的变量 ThreadLocals, 这个就是用来保存每个线程的私有数据 。
精通高并发与多线程,却不会用ThreadLocal?文章插图
ThreadLocalMapThreadLocalMap是ThreadLocal的内部类 , 每个数据用Entry保存 , 其中的Entry用一个键值对存储 , 键为ThreadLocal的引用 。
精通高并发与多线程,却不会用ThreadLocal?文章插图
我们可以看到 Entry 继承于WeakReference , 这是因为如果是强引用 , 即使把 ThreadLocal 设置为 null , GC 也不会回收 , 因为 ThreadLocalMap 对它有强引用 。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下 , 始终有强引用链 threadRef -> currentThread -> threadLocalMap -> entry , Entry就不会被回收(Entry中包括了ThreadLocal实例和value) , 导致Entry内存泄漏 。
精通高并发与多线程,却不会用ThreadLocal?文章插图
那是不是就是说如果使用了弱引用 , 就不会造成内存泄露 呢 , 这也是不正确的 。
因为如果我们没有手动删除 Entry 的情况下 , 此时 Entry 中的 key == null , 这个时候没有任何强引用指向 threaLocal 实例 , 所以 threadLocal 就可以顺利被 gc 回收 , 但是 value 不会被回收 , 而这块的 value 永远不会被访问到 , 因此会导致内存泄露
精通高并发与多线程,却不会用ThreadLocal?文章插图
接下来我们看下 ThreadLocalMap 的几个核心方法:
set 方法首先我们先看下源码:
public void set(T value) {// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);// 判断map是否存在if (map != null)// 存在则调用map.set设置此实体entrymap.set(this, value);else// 如果当前线程不存在ThreadLocalMap对象则调用createMap进行ThreadLocalMap对象的初始化// 并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中createMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {//这里的this是调用此方法的threadLocalt.threadLocals = new ThreadLocalMap(this, firstValue);}复制代码