47 天时间, 洒热血复习, 我成功 "挤进" 了字节跳动 (附 Java 面试题 + 学习笔记 + 算法刷题)zhuanlan.zhihu.com
图标
面试 "阿里云" 居然一面就惨被吊打? 幸终得内推机会, 4 面喜提华为 offerzhuanlan.zhihu.com
图标
关于 ThreadLocal
ThreadLocal 我们经常称之为线程本地变量, 通过它能够实现线程与变量之间的绑定, 也就是说每个线程只能读写本线程对应的变量. 对于同一个 ThreadLocal 对象, 每个线程对该对象读写时只能看到属于自己的变量, 这样来看 ThreadLocal 也是一种线程安全的模式. ThreadLocal 的功能如下图所示, 一个 ThreadLocal 对象就是一个线程本地变量, 该变量可以保存多个变量值, 比如线程一对应变量值一, 其它两个线程也有自己的变量值.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal 变量绑定 </figcaption>
ThreadLocal 例子
我们通过一个小例子来了解 ThreadLocal 的使用方法. 首先创建一个 ThreadLocal 对象, 由于是泛型所以需要指定保存的数据类型, 这里保存的是 String 类型. 然后启动五个线程, 每个线程都通过 ThreadLocal 对象的 set 方法设置要绑定该线程的变量值, 要保存什么值就传入什么值, 而当我们要使用时则调用 ThreadLocal 对象的 get 方法, 该方法无需传入参数值. 最终的输出结果如下.
Thread-1--->Thread-1 的变量 Thread-0--->Thread-0 的变量 Thread-4--->Thread-4 的变量 Thread-3--->Thread-3 的变量 Thread-2--->Thread-2 的变量
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal 例子 </figcaption>
这个例子的效果如下图, 五个线程都各自有各自对应的变量.
image
ThreadLocal 三个主要方法
set 方法, 用于设置当前线程本地变量的值, 传入的参数为要设置的值. 比如 threadLocal.set("value") .
get 方法, 用于获取当前线程本地变量的值, 无需传入任何参数. 比如 String threadLocalValue = (String) threadLocal.get() .
remove 方法, 用于删除当前线程本地变量, 无需传入任何参数. 比如 threadLocal.remove() .
如何模拟实现
在了解了 ThreadLocal 的功能后我们试着想一个问题: ThreadLocal 是如何实现的呢, 变量与线程之间如何绑定的呢? 实际上, 如果让我们自己来实现 ThreadLocal 功能, 我们只要通过一个 Map 结构就能实现该功能了. 其中 Map 的 key 是当前线程, 而 Map 的 value 则是变量值. 下图展示了 ThreadLocal 的设计思想.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);"> 模拟实现 </figcaption>
模拟实现
再看具体的模拟实现代码, 该模拟类提供了 set,get 和 remove 三个方法, 这三个方法都是间接操作 Map 对象. 注意 Map 对象的 key 值都是当前线程, 由 Thread.currentThread() 来获取, 这个 key 值不必由调用方传入. 这样就实现了一个简单的 ThreadLocal, 是不是很简单?
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);"> 模拟实现 </figcaption>
JDK 中 ThreadLocal 的实现思想
上面的实现方式虽然简单且符合我们的思考方式, 但是它存在多线程并发性能问题, 这个怎么说呢? 其实很明显, 我们实现的 ThreadLocal 内部使用了一个 Map 对象, 所有线程的操作都是针对该 Map 对象进行的操作, 需要保证该对象访问的线程安全, 这就需要额外的锁机制来保证, 但与此同时也就带来了性能问题.
JDK 为我们提供的 ThreadLocal 的实现则比较巧妙, 为了避免并发时涉及锁问题, 它在每个线程对象中都放一个 Map 对象, 但它并没有直接使用 JDK 的 Map 类, 而是自己实现了一个 key-value 数据结构. 每个线程都操作自己的 Map 对象则不存在并发问题, 如下图, 线程一包含了一个 Map 对象, 该 Map 对象的 key 是 ThreadLocal 对象, 而 value 则是变量值. 注意这里的实现需要将思维转换一下, ThreadLocal 对象变成了 key, 也就是说可能存在很多不同的 ThreadLocal 对象, 要查找时需要传入对应的 ThreadLocal 对象.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal 实现思想 </figcaption>
JDK 的实现源码分析
注意这里只分析实现的核心内容, 并非包括所有源码细节, 并且为了达到简洁清晰的效果, 可能会删除或修改少量源码. 我们先来看 Thread 类与 ThreadLocal 类的关系, 看到 Thread 类中包含了一个 threadLocals 变量, 它是一种 ThreadLocal.ThreadLocalMap 类型, 该类型定义在 ThreadLocal 类里面, 也就是一个内部类. 而 ThreadLocalMap 这个内部类即是实现了一个 Map 结构, 该类又包含了 Entry 内部类, ThreadLocal 对象和变量值则是通过 Entry 来保存.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);"> 类关系 </figcaption>
Thread 类里面声明了 threadLocals 变量用于关联 ThreadLocal.ThreadLocalMap 对象, 注意默认为 null.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">Thread 类 </figcaption>
而 ThreadLocal 类的大体结构如下, 提供了主要的三个方法, 其 ThreadLocalMap 内部类实现 Map 结构. Map 结构具体由 Entry 类实现, 该类继承了 WeakReference 类, 目的是为了避免内存泄漏. 下面将对三个主要方法进行分析.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">ThreadLocal 类 </figcaption>
对于多个线程与多个线程本地变量来说, 它们的结构如下图.
image
关于 ThreadLocalMap 类
ThreadLocalMap 类实际上就是一个 Map 结构的实现, 对于 Java 开发人员来说对 Map 再熟悉不过了, 而且由于 ThreadLocalMap 类的实现涉及到很多细节, 如果我们纯讲它繁琐的实现源码则会导致篇幅冗长, 所以这里我们主要是了解它的结构和操作即可. ThreadLocalMap 类使用数组来保存 key-value, 数组的每个元素对应一个 key-value, 所以新增, 修改, 删除等操作都是围绕着数组进行的. 保存之前会先用哈希算法计算线程对象的哈希值, 这是一个整型值, 通过该值就能定位数组的某个位置的元素, 这样就能找到对应的 key-value 进行操作.
image
ThreadLocal 的 set 方法
我们看 set 方法的实现, ThreadLocal 类的 set 方法逻辑为: 首先获取当前线程对象, 然后通过 getMap 方法获取当前线程的 ThreadLocalMap, 其实就是从 Thread 对象中获取, 最后调用 ThreadLocalMap 对象的 set 方法保存 key-value. 注意如果 Thread 对象中的 ThreadLocalMap 对象为空的话则需要调用 createMap 方法先创建 ThreadLocalMap 对象并关联到 Thread 对象中.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">set 方法 </figcaption>
ThreadLocal 的 get 方法
get 方法的逻辑为: 首先获取当前线程对象, 然后通过 getMap 方法获取当前线程的 ThreadLocalMap 对象, 如果该对象不为空则调用 ThreadLocalMap 对象的 getEntry 方法获取 Entry,Entry 对象即包含了我们要的 value. 如果获取不到值则最终还会执行 setInitialValue 方法, 它是根据 ThreadLocal 对象的 initialValue 方法来设置初始值, 默认是 null, 如果你想要设置一个初始值则可以重写 initialValue 方法.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">get 方法 </figcaption>
ThreadLocal 的 remove 方法
remove 方法的逻辑很简单, 直接获取当前线程对象的 ThreadLocalMap 对象, 然后调用该对象的 remove 方法删除对应的 key-value.
image
<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">remove 方法 </figcaption>
ThreadLocal 的内存泄漏
JDK 的实现是让 Entry 继承了 WeakReference 类, 所以可以指定对某个对象进行弱引用, 弱引用类型在没有其它强引用的情况下会被 JVM 的垃圾回收器回收. 我们通过下图来理解如何导致内存泄漏, 我们知道 ThreadLocal 被创建后就会伴随 Thread 的整个生命周期, 假如这个线程的生命周期很长则会导致严重的内存泄漏, 下面看具体的情况.
运行栈运行过程中假如某个时刻 ThreadLocal 引用不再指向 ThreadLocal 对象, 则该对象仅仅剩下一个弱引用, 这时该对象就会被 JVM 回收, 从而导致 Entry 的 key 为 null,key 为 null 时就导致 ThreadLocalMap 无法再找到这个 Entry 的 value. 一旦运行时间被拉长, value 将一直存在内存中而无法被回收, 这样就造成了内存泄漏, 整个引用关系为 Thread 对象 ->ThreadLocalMap 对象 ->Entry 对象 ->value.
image
那是不是不要继承 WeakReference 类, 让它默认强引用就不会导致内存泄漏呢? 那肯定不是, 不然也就不用多此一举了. 运行栈运行过程中假如某个时刻 ThreadLocal 引用不再指向 ThreadLocal 对象, 则 ThreadLocal 对象因为存在强引用而不被 JVM 回收, 此时除了 value 无法被回收外, ThreadLocal 对象也无法被回收, 同样产生内存泄漏问题.
综上所述, 不管 Entry 有没有继承 WeakReference 类都存在内存泄漏问题, 如果我们不手动去执行 remove 操作的话都会导致内存泄漏. 那么 JDK 团队为什么又要继承 WeakReference 类呢? 那是因为他们想采取一些措施来尽量保证内存不泄漏, 也就是说他们会在 ThreadLocalMap 类的 get,set,remove 方法中去执行一个清除操作, 把 ThreadLocalMap 包含的所有 Entry 中 key 为 null 的 value 给清除掉, 并且将对应的 Entry 也置为 null, 以便被 JVM 回收.
所以我们在使用 ThreadLocal 时要注意的一点是: 当我们使用完 ThreadLocal 时都要手动调用 remove 方法, 从而避免内存泄漏.
总结
本篇文章介绍了 ThreadLocal 的相关知识, 从简单的使用例子开始一步一步深入, 而且我们还自己模拟实现了一个 ThreadLocal 类, 模拟的方式简洁且容易理解, 但却存在并发性能问题, 所以 JDK 实现的 ThreadLocal 相对复杂很多. 然后我们分析了 JDK 的 ThreadLocal 的实现思想, 最后从源码级别分析它的实现, 包括 set,get 和 remove 三个主要方法. 最后, 我们讲解了 ThreadLocal 存在的内存泄漏问题, 并提出了使用 ThreadLocal 的注意点是要手动调用 remove 方法清理掉不再使用的 key-value.
来源: http://www.jianshu.com/p/812555ba4194