【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();
    }
}
相关推荐
V+zmm1013431 分钟前
基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
Oneforlove_twoforjob1 小时前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
xmh-sxh-13141 小时前
常用的缓存技术都有哪些
java
搬码后生仔1 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
凡人的AI工具箱1 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
AiFlutter1 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
J不A秃V头A2 小时前
IntelliJ IDEA中设置激活的profile
java·intellij-idea
DARLING Zero two♡2 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
Lx3522 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生2 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端