面试题
- ThreadLocal中ThreadLocalMap的数据结构和关系
- ThreadLocal的key是弱引用, 这是为什么?
- ThreadLocal内存泄漏问题你知道吗?
- ThreadLocal中最后为什么要加remove方法?
1. ThreadLocal简介
ThreadLocal 提供线程局部变量。这些变量与正常的变量不同, 因为每一个线程在访问 ThreadLocal 实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段, 使用它的目的是希望将状态(例如, 用户ID或事物ID)与线程关联起来。
|
如果不在 finally 块中调用 remove 会导致原先的值没有被清理, 默认值不再是 0, 而是上一个线程设置的值
因为每个 Thread 内有自己的实例副本且该副本只有当前线程自己使用
既然其他 ThreadLocal 不可访问, 那就不存在多线程间共享问题
统一设置初始值, 但是每个线程对这个值得修改都是各自线程互相独立的
如何才能不争抢
加入synchronized或者Lock控制资源的访问顺序
ThreadLocal
2. ThreadLocal源码分析
2.1 Thread、ThreadLocal、ThreadLocalMap关系
- Thread 对象维护着一个 ThreadLocalMap 的引用
- ThreadLocalMap 是 ThreadLocal 的内部类, 用 Entry 来进行存储值
- 调用 ThreadLocal 的 set() 方法时, 实际上就是往 ThreadLocalMap 设置值, key 是 ThreadLocal 对象, 值 Value 是传递进来的对象
- 调用 ThreadLocal 的 get() 方法时, 实际上就是从 ThreadLocalMap 获取值, key 是 ThreadLocal 对象
2.2 源码分析
ThreadLocalMap 成员变量
|
ThreadLocal#get()
源码
|
此处说明 ThreadLocalMap 采用的是懒加载模式, 用时再去初始化
ThreadLocalMap#getEntry()
源码
|
Entry 是 ThreadLocalMap 的内部类, 继承自
WeakReference<ThreadLocal<?>>
, 因此e.get()
获取的就是该 Entry 对应的 ThreadLocal算出当前 key 在数组中下标 i, 通过当前线程局部变量的线程局部哈希码与 数组长度 - 1 做与运算。根据下标 i 获取对象 e, 如果 e 不为 null 且 e 的 key 等于当前线程局部变量, 则返回 e, 否则开启线性探测
线程局部哈希码 threadLocalHashCode 源码
|
这里定义了一个 AtomicInteger 类型, 每次获取当前值并加上 HASH_INCREMENT, HASH_INCREMENT = 0x61c88647, 这个值和斐波那契散列有关(这是一种乘数散列法, 只不过这个乘数比较特殊, 是2^32乘以黄金分割比例的值, 即是1640531527, 16进制表示为 0x61c88647, 其主要目的就是为了让哈希码能均匀的分布在 2 的 n 次方的数组里, 也就是 Entry[] table 中, 这样做可以尽量避免 hash 冲突
ThreadLocalMap#getEntryAfterMiss()
源码
|
开启 while 循环, 当 e 的 key 与当前线程对象相等时, 返回 e
如果 e 的 key 等于 null, 开启探测式清理, 也就是 expungeStaleEntry() 方法
如果 e 的 key 不为 null, 且不等于当前线程对象, 说明哈希冲突了, 开启线性探测
ThreadLocalMap#expungeStaleEntry()
源码
|
当前位置的 key 等于 null, value 也置为 null, entry 也置为 null, size—
从当前位置的下一个位置 i 开始循环, 如果 e 等于 null, 退出循环, 否则向 i 的下一个位置继续循环。
如果 e 的 key 等于 null, value 也置为 null, entry 也置为 null, size–-。不为 null, 则计算当前线程局部变量的下标 h。如果 h 与 i (当前下标)不相等, 说明当时插入的时候发生了哈希冲突。开始 rehash, 将当前下标的 entry 清空, 从 h 开始寻找空位, 找到空位后插入当前对象。如果恰好 h 下标为 null, 那么当前线程局部变量则不会发生哈希冲突, 直接插入到 h 位置。
HashMap与ThreadLocalMap解决哈希冲突的区别:
- HashMap 使用的是链地址法, 当发生哈希冲突的时候, 使用链表来存储相同哈希码的数据, 满足一定条件, 还可以链表红黑树互相转换
- ThreadLocalMap 使用的是线性探测法, 当发生哈希冲突的时候, 就往后寻找空位置, 直到寻找到空位置插入。
ThreadLocal#setInitialValue()
源码
|
ThreadLocal#createMap()
源码
|
ThreadLocal#set()
源码
|
ThreadLocalMap#set()
源码
|
ThreadLocal#remove()
源码
|
ThreadLocal#remove()
源码
|
3. ThreadLocal内存泄漏
- 使用
WeakReference<ThreadLocal<?>>
将 ThreadLocal 对象变成一个弱引用对象 - 定义 Entry 类来继承
WeakReference<ThreadLocal<?>>
3.1 强、软、弱、虚引用
Java 允许使用
finalize()
方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作
强引用
- 对于强引用的对象, 就算是出现了 OOM 也不会对该对象进行回收
- 当一个对象被强引用变量引用时, 它处于可达状态, 是不可能被垃圾回收机制回收的, 即使该对象以后永远都不会被用到, JVM 也不会回收, 因此强引用是造成 Java 内存泄露的主要原因之一
|
软引用
- 是一种相对强引用弱化了一些的引用, 需要使用
java.lang.ref.SoftReference
类来实现, 可以让对象豁免一些垃圾回收 - 对于只有软引用的对象而言, 当系统内存充足时, 不会被回收, 当系统内存不足时, 才会被回收
- 软引用通常用在对内存敏感的程序中, 比如高速缓存, 内存够用就保留, 不够用就回收。
|
假如有一个应用需要读取大量的本地图片:
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中又可能造成内存溢出。此时使用软引用可以解决这个问题。
设计思路是: 用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系, 在内存不足时, JVM会自动回收这些缓存图片对象所占用的空间, 从而有效地避免了OOM的问题。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
弱引用
- 使用
java.lang.ref.WeakReference
类来实现, 比软引用的生命周期更短, 对于只有弱引用的对象而言, 只要垃圾回收机制一运行, 不管JVM的内存空间是否足够, 都会回收该对象占用的内存。
|
虚引用
- 虚引用必须和引用队列(
ReferenceQueue
)联合使用- 虚引用需要
java.lang.ref.PhantomReference
类来实现, 与其他引用都不同, 虚引用并不决定对象的生命周期。如果一个对象仅持有虚引用, 那么它就和没有任何引用一样, 在任何时候都有可能被垃圾回收器回收, 它不能单独使用也不能通过它访问对象。
- 虚引用需要
PhantomReference#get()
总是返回 null- 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 后, 做某些事情的通知机制。
- 处理监控通知使用
- 设置虚引用关联对象的唯一目的, 就是在对象被 GC 的时候会收到一个系统通知或者后续添加进一步的处理, 用来实现比 finalize 机制更灵活的回收操作。
构造方法
|
示例
|
3.2 为什么使用弱引用
为什么要用弱引用:
- 当方法执行完毕后, 栈帧销毁, 强引用 tl 也就没有了, 但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象
- 若这个 key 是强引用, 就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收, 造成内存泄露
- 使用弱引用就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null
但如果当前线程迟迟不结束(例如线程池场景), 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链
- value 对象需要 ThreadLocalMap 调用 get, set 时发现 key 为 null 才会去回收整个 entry, 因此弱引用不能 100% 保证内存不泄露, 我们要在不使用某个 ThreadLocal 对象后, 手动调用 remove 方法来删除它
在线程池中, 不仅仅是内存泄漏的问题, 因为线程池中的线程是重复使用的, 意味着这个线程的 ThreadLocalMap 对象也是重复使用的, 如果我们不手动调用 remove 方法, 那么后面的线程就有可能获取到上个线程遗留下来的 value 值
此后调用 get, set, remove 方法时, 就会尝试删除 key 为 null 的 Entry, 从而释放 value 对象占用的内存
- 通过
expungeStaleEntry
,cleanSomeSlots
,replaceStaleEntry
这三个方法回收键为 null 的 Entry 对象的值(即value)以及 entry 对象本身从而防止内存泄漏, 属于安全加固的方法
- 通过