【ThreadLocal】实现原理

在上期,介绍了ThreadLocal的概述以及基本用法后,我们本期则钻研一下ThreadLocal是怎么实现以及内部工作原理。

ThreadLocal存放位置

首先来了解一下ThreadLocal是存放在内存中的哪个位置的,调用ThreadLocal的set方法时,可以看到一个跟ThreadLocal相似的一个类:ThreadLocalMap
image.png

ThreadLocalMap的底层数据结构

ThreadLocalMap的结构与日常使用的HashMap类似,底层都是使用数组进行存储。唯一不同的点就是底层的数组对象类型为了避免内存泄漏,继承了弱引用。但是在使用不当的情况下,弱引用也无法解决内存泄漏的问题。
image.png
除了存储ThreadLocal的Entry数组之外,就只有size以及扩容的阈值了。
image.png
整个ThreadLocalMap的内部属性也是比较少的。

hash算法

我们想快速的在数组中找到该元素存放的位置的话,就需要一个公式的存在。传入一个固定的参数,返回的结果也是固定的,那么就再极短的时间内找到存放的位置。无需遍历整个数组,浪费时间了,这也是hash算法出现的原因。
在ThreadLocalMap中,使用的算法是:

int index = threadLocalHashCode & (tableLength - 1)

根据ThreadLocal的一个属性threadLocalHashCode数组的长度-1进行与运算,计算出该元素应该存放的位置。
由于Entry并非为链式对象,所以计算出来的位置可能已经被占用了,此时ThreadLocal的操作就是从该位置往后找,直至找到一个空的位置进行存放。

ThreadLocalMap的扩容机制

一般来说,ThreadLocal的数量不会太多,要是真的使用过多那就有可能是程序在设计之初就有问题了。不过还是看一下ThreadLocalMap是怎么扩容的。

名词描述

有效的ThreadLocal:指Entry对象引用的ThreadLocal仍未被回收,不是null值

扩容路径

扩容机制由resize方法实现,该方法的唯一入口在set方法中。这个也合理,在新增或重新设置值时发现数组中有效的ThreadLocal数量超过阈值时,就进行扩容。

set() -> 当前大小超过阈值 -> rehash() -> 删除空的Entry对象后,仍超过阈值的四分之三 -> resize()

所以实际上扩容有两个前提

  1. 调用set方法时数组中有效的ThreadLocal数量超过阈值
  2. 清理一波后,数组中有效的ThreadLocal数量仍超过阈值的四分之三

HashMap的阈值75%不一样,ThreadLocalMap阈值为数组大小的三分之二。而且在超过阈值时,会针对数组进行一波清理,要是在清理过后,还是超过阈值的四分之三,才真正进行扩容。

扩容逻辑

image.png

  1. 创建一个新数组,长度为旧数组的两倍
  2. 遍历旧数组,将有效的ThreadLocal根据hash算法计算出新的位置并存放。
  3. 重新设置阈值以及数量等属性

ThreadLocal的生命周期

ThreadLocal从它的名字就能简单的得出一个结论:

ThreadLocal的生命周期一定是与线程息息相关的

与线程的关系

我们来回顾一下ThreadLocal的存放位置
image.png
ThreadLocal间接被线程引用了,所以在引用关系不被打破时,ThreadLocal的生命周期与线程的生命周期是一致的。
那么问题就变成了线程的生命周期有多长呢?
众所周知,只要线程中的代码片段还在执行的话,那么线程就会一直存在。


上面提到了线程和ThreadLocal是有引用关系的,那么这个引用关系在什么时候被打破了呢?
有一个关键点:Entry类继承了弱引用并且ThreadLocal正是被引用的对象
所以在每次GC的时候垃圾回收器都会尝试将引用的对象(ThreadLocal)给回收掉。

有关Java中的引用原理可以去我的专栏《Java引用关系》了解一下

弱引用在处理引用对象的回收时,会先去看引用对象是否还在被使用。若是没有在使用,就会在GC的时候被回收了。

总结

ThreadLocal的生命周期分为两个阶段:

  1. 线程还在运行并且线程中变量仍在使用ThreadLocal,那么ThreadLocal就会一直存在
  2. 线程还在运行,但是线程上下文没有使用ThreadLocal了,那么ThreadLocal就会被回收。

在第2点中,虽然ThreadLocal没有被使用并且被垃圾回收器回收掉了,要是没有调用remove方法的话,就会出现内存泄漏的问题了。

内存泄漏问题及解决方案

在ThreadLocal的生命周期中提到了,没有显示调用remove方法时,会造成内存泄漏。那么我们来看一下为什么会产生内存泄漏呢?

问题

先提供一段会产生内存泄漏的问题代码:

public static void main(String[] args) {
    new Thread(() -> {
        while (true) {
            ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
            threadLocal.set(new int[1 * 1024 * 1024]);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }, "ThreadLocal").start();
}

说明一下这段代码的作用:
创建了一个新线程,在线程中,每隔1秒创建一个4M大小的数组并把数组放进ThreadLocal中。

产生原因

在代码运行过程中,把内存快照给dump下来然后导入到MAT中打开,我们就能得知ThreadLocal的存活情况了。
如何找到线程中的ThreadLocalMap对象呢?
先点击工具栏上的小齿轮按钮image.png,找到对应的线程。image.png
就能在左边信息栏中找到线程中threadLocals属性了,紧接着用内存地址找到对应的ThreadLocalMap对象。
image.png
这是运行了一段时间后的ThreadLocalMap对象,可以看到已经存放了许多的Entry对象了。此时,我们随机选一个Entry对象来分析一下。
image.png
这是其中的一个Entry对象属性,可以看到value是创建的int数组,但是referent却是null。
说明ThreadLocal已经被垃圾回收器回收掉了,但是int数组还存在着。
回到之前ThreadLocal的存放位置的图中,此时ThreadLocal已经被回收了,但是Entry对象中还存在着value属性的引用关系,所以Entry对象无法被正常回收,出现了内存泄漏的问题。
image.png
初步总结一下:运行过程中,ThreadLocal对象被回收掉了,但是所存储的对象却没有被回收,出现了内存泄漏问题了。


但是,为什么上面的程序运行了很久,都没有出现OOM呢?
先说结论,那是因为我们每隔1秒就创建一个ThreadLocal对象,在调用set方法时,ThreadLocal内部条件达到后会进行数组的清理工作,致使程序在运行过程中没有出现OOM。
有关内部的清理逻辑,会开一篇新的文章进行说明。

解决方案

解决ThreadLocal的内存泄漏问题解决方案很简单,不再使用的时候及时调用remove方法即可。
针对一般Web程序来说,我们都会在拦截器Filter中进行ThreadLocal的初始化以及清空操作,只需要在初始化后,finally中调用remove方法就可以了。

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    ThreadLocal<String> threadLocal = new ThreadLocal<>();
    try {
        chain.doFilter(request, response);
    } finally {
        threadLocal.remove();
    }
}
  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值