【ThreadLocal】实现原理

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

ThreadLocal存放位置

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

ThreadLocalMap的底层数据结构

ThreadLocalMap的结构与日常使用的HashMap类似,底层都是使用数组进行存储。唯一不同的点就是底层的数组对象类型为了避免内存泄漏,继承了弱引用 。但是在使用不当的情况下,弱引用也无法解决内存泄漏的问题。

除了存储ThreadLocal的Entry数组之外,就只有size以及扩容的阈值了。

整个ThreadLocalMap的内部属性也是比较少的。

hash算法

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

在ThreadLocalMap中,使用的算法是:

java 复制代码
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阈值为数组大小的三分之二。而且在超过阈值时,会针对数组进行一波清理,要是在清理过后,还是超过阈值的四分之三,才真正进行扩容。

扩容逻辑

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

ThreadLocal的生命周期

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

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

与线程的关系

我们来回顾一下ThreadLocal的存放位置

ThreadLocal间接被线程引用了,所以在引用关系不被打破时,ThreadLocal的生命周期与线程的生命周期是一致的。

那么问题就变成了线程的生命周期有多长呢?

众所周知,只要线程中的代码片段还在执行的话,那么线程就会一直存在。


上面提到了线程和ThreadLocal是有引用关系的,那么这个引用关系在什么时候被打破了呢?

有一个关键点:Entry类继承了弱引用 并且ThreadLocal正是被引用的对象

所以在每次GC的时候垃圾回收器都会尝试将引用的对象(ThreadLocal)给回收掉。

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

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

总结

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

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

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

内存泄漏问题及解决方案

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

问题

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

java 复制代码
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 对象呢?

先点击工具栏上的小齿轮按钮,找到对应的线程。

就能在左边信息栏中找到线程中threadLocals属性了,紧接着用内存地址找到对应的ThreadLocalMap对象。

这是运行了一段时间后的ThreadLocalMap对象,可以看到已经存放了许多的Entry对象了。此时,我们随机选一个Entry对象来分析一下。

这是其中的一个Entry对象属性,可以看到value是创建的int数组,但是referent却是null。

说明ThreadLocal已经被垃圾回收器回收掉了 ,但是int数组还存在着。

回到之前ThreadLocal的存放位置的图中,此时ThreadLocal已经被回收了,但是Entry对象中还存在着value属性的引用关系 ,所以Entry对象无法被正常回收,出现了内存泄漏的问题。

初步总结一下:运行过程中,ThreadLocal对象被回收掉了,但是所存储的对象却没有被回收,出现了内存泄漏问题了。


但是,为什么上面的程序运行了很久,都没有出现OOM呢?

先说结论,那是因为我们每隔1秒就创建一个ThreadLocal对象,在调用set方法时,ThreadLocal内部条件达到后会进行数组的清理工作 ,致使程序在运行过程中没有出现OOM。

有关内部的清理逻辑,会开一篇新的文章进行说明。

解决方案

解决ThreadLocal的内存泄漏问题解决方案很简单,不再使用的时候及时调用remove方法即可。

针对一般Web程序来说,我们都会在拦截器Filter中进行ThreadLocal的初始化以及清空操作,只需要在初始化后,finally中调用remove方法就可以了。

java 复制代码
@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();
    }
}
相关推荐
一只叫煤球的猫13 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz96513 小时前
tcp/ip 中的多路复用
后端
bobz96513 小时前
tls ingress 简单记录
后端
皮皮林55115 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友15 小时前
什么是OpenSSL
后端·安全·程序员
bobz96515 小时前
mcp 直接操作浏览器
后端
前端小张同学17 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook17 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康18 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在18 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net