你好,我是yes。
今天我们再来盘一盘 ThreadLocal ,这篇力求对 ThreadLocal 一网打尽,彻底弄懂 ThreadLocal 的机制。
有了这篇基础之后,下篇再来盘一盘 ThreadLocal 的进阶版,等我哈。
话不多说,本文要解决的问题如下:
- 为什么需要 ThreadLocal
- 应该如何设计 ThreadLocal
- 从源码看ThreadLocal 的原理
- ThreadLocal 内存泄露之为什么要用弱引用
- ThreadLocal 的最佳实践
- InheritableThreadLocal
好了,开车!
为什么需要 ThreadLocal
最近不是开放三胎政策嘛,假设你有三个孩子。
现在你带着三个孩子出去逛街,路过了玩具店,三个孩子都看中了一款变形金刚。
所以你买了一个变形金刚,打算让三个孩子轮着玩。
回到家你发现,孩子因为这个玩具吵架了,三个都争着要玩,谁也不让着谁。
这时候怎么办呢?你可以去拉架,去讲道理,说服孩子轮流玩,但这很累。
所以一个简单的办法就是出去再买两个变形金刚,这样三个孩子都有各自的变形金刚,世界就暂时得到了安宁。
映射到我们今天的主题,变形金刚就是共享变量,孩子就是程序运行的线程。有多个线程(孩子),争抢同一个共享变量(玩具),就会产生冲突,而程序的解决办法是加锁(父母说服,讲道理,轮流玩),但加锁就意味着性能的消耗(父母比较累)。
所以有一种解决办法就是避免共享(让每个孩子都各自拥有一个变形金刚),这样线程之间就不需要竞争共享变量(孩子之间就不会争抢)。
所以为什么需要 ThreadLocal?
就是为了通过本地化资源来避免共享,避免了多线程竞争导致的锁等消耗。
这里需要强调一下,不是说任何东西都能直接通过避免共享来解决,因为有些时候就必须共享。
举个例子:当利用多线程同时累加一个变量的时候,此时就必须共享,因为一个线程的对变量的修改需要影响要另个线程,不然累加的结果就不对了。
再举个不需要共享的例子:比如现在每个线程需要判断当前请求的用户来进行权限判断,那这个用户信息其实就不需要共享,因为每个线程只需要管自己当前执行操作的用户信息,跟别的用户不需要有交集。
好了,道理很简单,这下子想必你已经清晰了 ThreadLocal 出现的缘由了。
再来看一下 ThreadLocal 使用的小 demo。
public class YesThreadLocal { private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> Thread.currentThread().getName()); public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(() -> { System.out.println("threadName: " + threadLocalName.get()); }, "yes-thread-" + i).start(); } } }
输出结果如下:
可以看到,我在 new 线程的时候,设置了每个线程名,每个线程都操作同一个 ThreadLocal 对象的 get 却返回的各自的线程名,是不是很神奇?
应该如何设计 ThreadLocal ?
那应该怎么设计 ThreadLocal 来实现以上的操作,即本地化资源呢?
我们的目标已经明确了,就是用 ThreadLocal 变量来实现线程隔离。
从代码上看,可能最直接的实现方法就是将 ThreadLocal 看做一个 map ,然后每个线程是 key,这样每个线程去调用 ThreadLocal.get
的时候,将自身作为 key 去 map 找,这样就能获取各自的值了!
听起来很完美?错了!
这样 ThreadLocal 就变成共享变量了,多个线程竞争 ThreadLocal ,那就得保证 ThreadLocal 的并发安全,那就得加锁了,这样绕了一圈就又回去了!
所以这个方案不行,那应该怎么做?
答案其实上面已经讲了,是需要在每个线程的本地都存一份值,说白了就是每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,所以怎么弄呢?
在线程对象内部搞个 map,把 ThreadLocal 对象自身作为 key,把它的值作为 map 的值。
这样每个线程可以利用同一个对象作为 key ,去各自的 map 中找到对应的值。
这不就完美了嘛!比如我现在有 3 个 ThreadLocal 对象,2 个线程。
ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
那此时 ThreadLocal 对象和线程的关系如下图所示:
这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求,完美!
JDK就是这样实现的!我们来看看源码。
从源码看ThreadLocal 的原理
前面我们说到 Thread 对象里面会有个 map,用来保存本地变量。
我们来看下 jdk 的 Thread 实现
public class Thread implements Runnable { // 这就是我们说的那个 map 。 ThreadLocal.ThreadLocalMap threadLocals = null; } 复制代码
可以看到,确实有个 map ,不过这个 map 是 ThreadLocal 的静态内部类,记住这个变量的名字 threadLocals,下面会有用的哈。
看到这里,想必有很多小伙伴会产生一个疑问。
竟然这个 map 是放在 Thread 里面使用,那为什么要定义成 ThreadLocal 的静态内部类呢?
首先内部类这个东西是编译层面的概念,就像语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和平日里外部定义的类没有区别,也就是说在 JVM 中是没有内部类这个概念的。
一般情况下非静态内部类用在内部类,跟其他类无任何关联,专属于这个外部类使用,并且也便于调用外部类的成员变量和方法,比较方便。
而静态外部类其实就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。
所以说这样定义的用意就是说明 ThreadLocalMap 是和 ThreadLocal 强相关的,专用于保存线程本地变量。
现在我们来看一下 ThreadLocalMap 的定义:
重点我已经标出来了,首先可以看到这个 ThreadLocalMap 里面有个 Entry 数组,熟悉 HashMap 的小伙伴可能有点感觉了。
这个 Entry 继承了 WeakReference 即弱引用。这里需要注意,不是说 Entry 自己是弱引用,看到我标注的 Entry 构造函数的 super(k)
没,这个 key 才是弱引用。
所以 ThreadLocalMap 里有个 Entry 的数组,这个 Entry 的 key 就是 ThreadLocal 对象,value 就是我们需要保存的值。
那是如何通过 key 在数组中找到 Entry 然后得到 value 的呢 ?
这就要从上面的 threadLocalName.get()
说起,不记得这个代码的滑上去看下示例,其实就是调用 ThreadLocal 的 get 方法。
此时就进入 ThreadLocal#get
方法中了,这里就可以得知为什么不同的线程对同一个 ThreadLocal 对象调用 get 方法竟然能得到不同的值了。
这个中文注释想必很清晰了吧!ThreadLocal#get
方法首先获取当前线程,然后得到当前线程的 ThreadLocalMap 变量即 threadLocals,然后将自己作为 key 从 ThreadLocalMap 中找到 Entry ,最终返回 Entry 里面的 value 值。
这里我们再看一下 key 是如何从 ThreadLocalMap 中找到 Entry 的,即map.getEntry(this)
是如何实现的,其实很简单。
可以看到 ThreadLocalMap 虽然和 HashMap 一样,都是基于数组实现的,但是它们对于 Hash 冲突的解决方法不一样,HashMap 是通过链表(红黑树)法来解决冲突,而 ThreadLocalMap 是通过开放寻址法来解决冲突。
听起来好像很高级,其实道理很简单,我们来看一张图就很清晰了。