ThreadLocal使用场景介绍以及关于内存泄漏的探讨

news2024/10/6 14:30:58

目录

1. 使用场景一:线程隔离

2. 使用场景二:使用ThreadLocal进行跨函数数据传递

3. ThreadLocal导致的内存泄漏问题

4. ThreadLocal在Spring框架中的应用

5. 扩展:InheritableThreadLocal


转载:【Java】ThreadLocal使用场景介绍以及关于内存泄漏的探讨 - 掘金

1. 使用场景一:线程隔离

【需求】假设我们有个UserService,[方法birthDate]中:

  • 通过用户id,拿到用户的生日。
  • 新建一个SimpleDateFormat对象df。
  • df.format(用户生日)。

【方式1】 如果我们新建10个线程,每个线程都在运行[方法birthDate],那么相当于每个线程都会新建一个df:

方式1导致的问题但如果我们有1000个task需要执行,那么直接创建1000个线程,显然不太合理,通常情况下,我们会用线程池的方式执行任务。

【方式2】 如果我们新建一个核心线程数为10的线程池,往里面提交1000个任务:

方式2导致的问题虽然我们使用了线程池的方式,线程数为10,但会创建1000个df对象。

【方式3】 那么我们将SimpleDateFormat提取到方法a的外面,然后用参数的形式传入。这样解决了每次都会创建df对象的开销,但是SimpleDateFormat是线程不安全的,即需要对这个对象加锁以保证线程安全。

方式3导致的问题给全局的SimpleDateFormat加锁,会使得同一时间只有一个线程能拿到这个对象,导致效率低下。

那么有没有一种更折中的方案,即既不需要在方法内创建df以致于极端情况下要多达1000次的创建,也不要只有1个df对象,以至于每个线程用到它的时候都要排队拿?

答案是有的,即使用ThreadLocal【方式4】 ):
由图可知,我们希望每个线程有自己的df对象,这样既不需要每个task都创建一次(节省了开销),也不需要每个thread相互抢一个df(提高了效率):

什么是ThreadLocal:如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

方式4的代码如下: 首先是新建一个Utils类,用来存放ThreadLocal,主要是重写了initialValue()方法,使得在新生成value的时候会自动生成一个SimpleDateFormat。

public class DateFormatThreadLocalUtils {

    public static final ThreadLocal<SimpleDateFormat> df = new ThreadLocal<>() {

        @Override
        protected SimpleDateFormat initialValue() {
            System.out.println("new SimpleDateFormat.....");
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

ps. 如果是JDK8+,可以用lmbda表达式写:

public class DateFormatThreadLocalUtils {
    public static ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}

UserService类:

  • 可以看到birthDate中使用到的sdf是从ThreadLocal中拿到的。
  • 如果上述的ThreadLocal没有重写initialValue方法,那么在使用的时候可以先判断从ThreadLocal get出来的SimpleDateFormat是否为空,如果为空,再new,再set回ThreadLocal中也是可以的。
public class UserService {

    public String birthDate(int userId) {
        Date birthDate = getBirthDay(userId);
        SimpleDateFormat sdf = DateFormatThreadLocalUtils.df.get();
        return sdf.format(birthDate);
    }

    public Date getBirthDay(int userId) {
        // todo, return a Date
    }

}

测试:Task有1000个,核心线程数为10,那么上述的SimpleDateFormat只会new 10次,因为它是每个线程独有的。

public class UserServiceMain {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i ++) {
            executorService.submit(() -> {
                String birthDate = (new UserService()).birthDate(id);
                System.out.println(Thread.currentThread().getName() + ": " + birthDate);
            });
        }
    }

}

【总结】Use Case-1其实就是用了空间换取时间,给每个thread都分配一个SimpleDateFormat实例,避免了线程间相互换资源造成的效率问题。

在上面的使用场景中,除了以上的例子,还可以为每个线程绑定一个数据库连接等。

2. 使用场景二:使用ThreadLocal进行跨函数数据传递

假设我们有个API,从前端接收到request,然后经过一系列个service,但每个service都需要user这个参数:

那么可以有几种实现:

  • 每个service的方法都带上user这个参数,以此来传递。
  • 可以新建一个ThreadLocal,然后在第1个service中将user值set到ThreadLocal中,往后的service就可以直接从ThreadLocal中获取。

代码示例:

public class UserThreadLocalUtils {
    public static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
}

比如我们写一个Filter,将userId存放到ThreadLocal中:

@Component
public class UserFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            String userId = request.getHeader("userId");
            UserThreadLocalUtils.USER_ID_HOLDER.set(userId);

            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            UserThreadLocalUtils.USER_ID_HOLDER.remove();
        }
    }
}

那么我们在Controller或是Service中都可以从ThreadLocal中拿:

@Slf4j
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("user")
    public boolean get() {
        log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());
        userService.get();
        return true;
    }

}
@Slf4j
@Service
public class UserService {

    public void get() {
        log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());
    }
}

 

3. ThreadLocal导致的内存泄漏问题

关于ThreadLocal的内存泄漏问题,可以参考以下,写的都非常好:

  • 博文:ThreadLocal内存泄露原因分析
  • 书《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》,第#1.8.7章

首先:

  • 什么是内存泄漏不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。
  • 强引用和弱引用(WeakReference),参考:blog.csdn.net/CSDN_DK317/…
    • 强引用即类似“Object obj=new Object()”这类的引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
    • 弱引用(WeakReference),在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

其次,书中,使用了一个小例子来解释了ThreadLocal中的引用关系:

当线程tn尝试跑上述方法时,会有两个引用:

  • 【引用1】 线程tn --> funcA() --> local -- (强引用) --> ThreadLocal实例。
  • 【引用2】 local.set(100) --> 线程tn --> ThreadLocalMap --> Entry实例 -- 其Key以(弱引用)包装的方式指向 --> ThreadLocal实例。

思考一个问题,【引用2】中为什么是弱引用?即为什么ThreadLocal中的实现,ThreadLocalMap中的key需要指向的ThreadLocal为弱引用?

  • 当方法funcA()执行完毕后,强引用local的值也就没有了。即【引用1】没有了。
  • 如果【引用2】的方式是强引用的话,那么就会造成ThreadLocal的实例回收,需要依赖线程tn的生命周期。 因此,【引用2】的Entry引用关系为弱引用,即ThreadLocal的实例回收不应该依赖tn线程的结束而回收。--> 即【引用1】的结束,就意味着【引用2】中Entry实例中的key指向ThreadLocal,会在下一次GC发生的时候,就回收掉了!

再思考一个问题,假设GC发生了,ThreadLocal对象被回收了(【引用1】的关系没有了,而【引用2】为弱引用),那么Entry中的key指向了null,那么这个Entry也就没有用了,它会在什么时候被释放?

  • 后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。--> 这也就是为什么我们需要调用remove()的原因。

最佳实践使用static + final修饰,并且调用remove()进行显示的释放操作

在《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》一书中关于ThreadLocal,建义使用static final修饰ThreadLocal对象:

使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。

此外,除了添加static final修饰外,还常常添加private,主要目的是缩小使用的范围,尽可能不让他人引用。

即如何在web项目中安全的使用ThreadLocal,可以考虑使用Filter,在finally的时候remove掉:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    try {
        //set ThreadLocal variable
        filterChain.doFilter(servletRequest, servletResponse);

    } finally {
        //remove threadLocal variable.
    }
}

4. ThreadLocal在Spring框架中的应用

Spring框架中也使用了很多ThreadLocal来hold一些context,如:

  • LocaleContextHolder
  • TransactionContextHolder
  • RequestContextHolder
  • SecurityContextHolder
  • DateTimeContextHolder

5. 扩展:InheritableThreadLocal

在JDK 1.2后,新引入了一个类,叫InheritableThreadLocal,这个类继承了ThreadLocal,从名字可以看出,Inheritable是可继承的意思。

ThreadLocal中,每一个线程在获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,自己也无法访问别人的ThreadLocalMap实例,达到相互隔离,互不干扰。

那么InheritableThreadLocal是线程中生成的子线程,也会共享该value。即在父线程中set的值,在子线程中通过get方法也可以获取到。

比如slf4j中的MDC类,有个变量叫MDCAdapter,它有个实现类叫BasicMDCAdapter,实质上就是一个InheritableThreadLocal

public class BasicMDCAdapter implements MDCAdapter {
    private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            return parentValue == null ? null : new HashMap(parentValue);
        }
    };
    ...
}

而MDC,在log中是很重要的,比如像sleuth中的trace_id的打印,用到的就是MDC,即我们可以往MDC中set trace_id,然后在日志appender中打印出来。

但是,InheritableThreadLocal会在某种情况下失效,即子线程并不能在所有场景下都能拿到父线程set的值。但也有解决方法,具体参考文章:

  • InheritableThreadLocal线程池下失效问题解决
  • 遇到线程池InheritableThreadLocal就废了,该怎么办?

作者:伊丽莎白2015
链接:https://juejin.cn/post/7132068313449889805
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727653.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

字节、数据位、ascii码、RS232串口、modbus协议

RS232通信协议详解 - 百度文库 ASCII_百度百科 ascii码表 modebus协议 - 百度文库https://wenku.baidu.com/view/58aae6134431b90d6c85c7b6.html?_wkts_1688713246949&bdQueryascii%E7%A0%81modelbus%E7%A0%81 Modbus 的RTU、ASCII、TCP傻傻搞不清楚&#xff1f;一文全…

归并排序的应用—逆序对的个数

一、&#xff08;题目&#xff09; 给定一个长度为 n 的整数数列&#xff0c;请你计算数列中的逆序对的数量。 逆序对的定义如下&#xff1a;对于数列的第 i个和第 j 个元素&#xff0c;如果满足 i<j且 a[i]>a[j]&#xff0c;则其为一个逆序对&#xff1b;否则不是。 输…

吐血整合!风控与反欺诈相关数据来源(上)

本文会分上、下两篇&#xff0c;介绍各种风控与反欺诈相关的数据来源&#xff0c;包括其主要的数据维度和产品服务形态。 目录&#xff1a; 人行二代征信 持牌征信机构数据 司法大数据 航旅大数据 铁路大数据 税务大数据 交通大数据 电力大数据 保险大数据 人社大数据 一、人行…

vs2019打包发布c#编写的exe应用

由于该应用还要引用其它的exe应用&#xff0c;所以 .NET Framework 4.6.2 脱机安装程序Windows - Microsoft 支持 参考下面的配置即可 VS2019如何打包程序_vs2019 打包_咻咻咻...的博客-CSDN博客https://blog.csdn.net/qq_37043193/article/details/119530253?ops_request_…

JVM源码剖析之Java对象创建过程

关于 "Java的对象创建" 这个话题分布在各种论坛、各种帖子&#xff0c;文章的水平参差不齐。并且大部分仅仅是总结 "面试宝典" 的流程&#xff0c;小部分就是copy其他帖子&#xff0c;极少能看到拿源码作为论证。所以特意写下这篇文章。 版本信息如下&…

搭建帮助中心5大注意事项

在现代互联网时代&#xff0c;为企业网站建立一个优雅实用的网站帮助中心变得尤为重要。一个好的网站帮助中心可以帮助企业解决客户的难点、痛点&#xff0c;提高客户满意度、期待值&#xff0c;从而更好地留住客户&#xff0c;增加收入。 如果没有帮助中心或者是帮助中心创建…

深入理解链表:一种动态的线性数据结构

文章目录 前言1. 概述2. 单向链表3. 单向链表&#xff08;带哨兵&#xff09;4. 双向链表&#xff08;带哨兵&#xff09;5. 环形链表&#xff08;带哨兵&#xff09;6. 结语 前言 链表是我们在日常编程中经常使用的一种数据结构&#xff0c;它相比于数组具有更好的动态性能。…

Spark(17):RDD、DataFrame、DataSet三者的关系

目录 0. 相关文章链接 1. 三者的产生 2. 三者的共性 3. 三者的区别 3.1. RDD 3.2. DataFrame 3.3. DataSet 4. 三者的互相转换 4.1. 互相转换图 4.2. DataFrame 和 DataSet 转换 0. 相关文章链接 Spark文章汇总 1. 三者的产生 在 SparkSQL 中 Spark 为我们提供了两…

Flutter TextField 输入框 简单使用

创建方式一&#xff1a; ///用于文本输入框 TextEditingController controller new TextEditingController();/// 设置TextField中显示的内容void setEditeInputTextFunction(String flagText) {controller .text flagText;}/// 清除TextField中显示的内容void clearEditeIn…

Web服务器群集:podman与docker技术集群

目录 一、理论 1.虚拟化 2.容器 3.podman 4.docker 5.podman与docker区别 二、实验 1.部署podman 2.部署docker 三、总结 一、理论 1.虚拟化 &#xff08;1&#xff09;概念 虚拟化&#xff1a;将应用程序和系统内核资源进行解耦&#xff0c;以操作系统级别进行隔离…

利用langchain-ChatGLM、langchain-TigerBot实现基于本地知识库的问答应用

目录 1 原理 2 langchain-ChatGLM的开发部署 2.1 安装环境 2.2 加载本地模型 3 langchain-TigerBot的开发部署 刷B站的时候&#xff0c;无意中看到吴恩达的一个langchain的教程&#xff0c;然后去github上搜了下&#xff0c;发现别人利用langchain和chatGLM做的基于本地知…

【C++11】lambda表达式 包装器

文章目录 1 lambda表达式1.1 引例1.2 lambda表达式的基本语法1.3 lambda表达式的底层原理 2 包装器3 bind 1 lambda表达式 1.1 引例 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法&#xff1a; #include <algorithm> …

docker运行 mycli

1. 制作镜像 1.1 Dockerfile: FROM python:3.8 ENV MYSQL_HOST192.168.1.108 ENV MYSQL_PWDroot RUN apt-get update && apt-get install -y less RUN pip3 install mycli ENTRYPOINT ["mycli"] 注意 python:3.8 pip3 mycli 具有版本限制的,如果改了版…

【计算机网络】1.5——计算机网络的体系结构(网络分层模型)

计算机网络的体系结构 概述 计算机网络的体系结构是计算机网络及其构建所应完成功能的精确定义 考题 不属于网络体系结构所描述的内容的是 A、网络的层次 B、每层使用的协议 C、协议的内部实现细节 D、每层必须完成的功能 这些功能的「实现细节」&#xff0c;是遵守这种体系…

Web 前端 Day 1

课程大纲&#xff1a; html 结构 css 表现 Js 行为 jquery库 &#xff08;地位下降趋势 仍旧在用&#xff09; bootstrap 前端高端只是&#xff1a;angular angularjs html 超文本标记语言 相关解释 描述网页的语言 不仅有文字&#xff0c;还有图片、音频、视频等等 超…

某网站JS加密、OB混淆与CSS反爬实战分析

1. 写在前面 最近一段时间接触了一些小说网站的业务。发现很多的小说网站&#xff0c;甚至一些小站它们的安全防护措施做的都很到位&#xff01;例如上次说到的的五秒盾也是存在于一个小说小站。今天要讲的这个网站它集JS加密、ob混淆、CSS反爬于一体 目标站点&#xff1a; aH…

用真人模型制作3D虚拟人物,岂不是更真实?

3D虚拟人物是指利用计算机技术和图形学技术创建的一种能够模拟真实人体形态、行为和语言的虚拟实体。与传统的平面图像或视频不同&#xff0c;3D虚拟人物具有立体感和真实感&#xff0c;能够在虚拟环境中实现人机交互和情感交流&#xff0c;给用户带来全新的沉浸式体验。 随着…

文件批量改名新技巧:轻松将日期插入到文件名中,整理更有序!

在数字化时代&#xff0c;我们每天都面临着大量的文件&#xff0c;而合理整理和命名这些文件对于我们的工作和生活至关重要。特别是在需要存档或分享文件时&#xff0c;具有清晰的命名规则可以极大地提升工作效率和组织性。 首先&#xff0c;进入文件批量改名高手的文件批量重…

表征材料表面性质​的第一性原理计算方法:功函数

功函数计算是指通过计算材料表面或界面上的电子结构能量差来确定材料的界面特性的方法。在材料科学和表面科学领域&#xff0c;界面的性质对于材料的功能和性能具有重要影响。通过理解和控制界面&#xff0c;可以优化材料的电子传输、光学性能、催化活性等关键特性。 功函数表示…

网络协议【图解TCP/IP(笔记二)】

文章目录 网络协议随处可见的协议协议的必要性生活中的协议计算机中的协议分组交换协议协议的标准化 网络协议 随处可见的协议 在计算机网络与信息通信领域里&#xff0c;人们经常提及“协议”一词。互联网中常用的具有代表性的协议有IP、TCP、HTTP等。而LAN&#xff08;局域…