最近接了一个新需求,业务场景上需要在原有基础上新增2个字段,接口新增参数意味着很多类和方法的逻辑都需要改变,需要先判断是否属于该业务场景,再做对应的逻辑。原本的打算是在入口处新增变量,在操作数据的时候进行逻辑判断将变量进行存储或查询。

如果全链路都变更入参和结构,很明显代码上很不优雅,后续如果还要增加业务场景,又需要再改一遍。如果有一个方法可以传递全局变量,而且仅限于当前线程就好了。

到此,会想到有两种解决方案:之前用的比较少的ThreadLocal或者使用redis缓存。考虑到新增字段都是些增删改查的操作,没有必要存到redis中,故使用ThreadLocal。

一、ThreadLocal定义

以微服务架构为例,服务提供方在收到调用方的请求后,会把这个请求分配给一个线程进行处理。一般来说,一个请求会一直由同一个线程处理,中间不会切换线程,所以如果有一个线程中共享的变量,可以当全局变量使用。

ThreadLocal实现的就是一个线程中的全局变量,与真正的全局变量的区别在于ThreadLocal的变量是每个线程中的全局变量,也就是说不同线程访问到的值是不一样的。其填充的变量属于当前线程,该变量对于其他线程是隔离的。

由定义可以发现,ThreadLocal有两个特性:每个Thread的变量只能由当前Thread使用;由于其他线程不可访问,则不存在多线程间共享的问题。

二、修饰

ThreadLocal提供了线程本地的实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。

ThreadLocal变量通常被private static修饰,这样的好处是当一个线程结束时,它所使用的ThreadLocal实例副本都可被回收,避免重复创建。坏处就是这样做可能正好导致内存泄漏。

三、底层实现

ThreadLocal最朴素的内部实现是Map<threadlocal, Object>,这是一个HashMap,又称为ThreadLocalMap。但Java源码并不是Map<threadlocal, Object>的实现。这是因为如果多个线程访问同一个map,这个map需要是线程安全的,构造比较麻烦。Java采用了更简单粗暴的做法:每个线程都有自己的ThreadLocal专属map,里面可以存放多个ThreadLocal变量,这样就解决了多线程同时操作一个map带来的多线程并发问题。

因为要把ThreadLocal的变量当做全局变量使用,需要把变量与初始化函数写在通用的类中,如DDD领域模型中写在Common模块。

具体的实现如下:

public class ThreadLocalUtil {

   private static ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();

   public static Integer getScene() {
return THREAD_LOCAL.get();
} public static void initScene(Integer scene) {
if (THREAD_LOCAL == null) {
THREAD_LOCAL = new ThreadLocal<>();
}
THREAD_LOCAL.set(scene);
} public static void remove() {
THREAD_LOCAL.remove();
}
}

四、致命点

上面提到了的ThreadLocal会带来内存泄露的问题,深入分析下:

一个ThreadLocal实例对应当前线程的一个对象实例,如果把ThreadLocal声明为某个类的实例变量不是静态变量,那么每次创建一个该类的实例就会导致一个新的对象实例被创建。而这些被创建的实例是同一个类的实例,于是同一个线程可能会访问到同一个类的不同实例,这即使不会导致错误,也会导致重复创建同样的对象。如果使用static修饰后,只要相应的类没有被垃圾回收掉,那么这个类就会持有相对应的ThreadLocal实例引用。

ThreadLocal自身并不存储值,而是作为一个key来让线程从ThreadLocal中获取value。ThreadLocalMap中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap中的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value却不为null。如果当前线程一直不结束或者线程结束后不被你销毁,这会产生内存泄露(已分配空间的堆内存由于某种原因未释放或无法释放导致系统内存浪费或程序运行变慢甚至系统奔溃)。

因此,key弱引用并不是导致内存泄露的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应的value

解决的方法也很简单,只需要打破引用路径中的ThreadLocalMap对对象实例的引用即可。也就是在使用完ThreadLocal之后,必须调用ThreadLocal.remove()。

延伸:

为什么要将Map中的key设置为弱引用呢?

实际上,设置key为弱引用能预防大多数内存泄露的情况。如果key使用强引用,引用的ThreadLocal对象被回收,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,也会导致内存泄露。设置为弱引用后,引用的ThreadLocal对象被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被java GC回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

参考文章:

https://www.cnblogs.com/tiancai/p/13141234.html?ivk_sa=1024320u

https://last2win.com/2020/09/05/java-threadlocal/

https://blog.csdn.net/u010445301/article/details/111322569

作者:京东零售 李泽阳

来源:京东云开发者社区 转载请注明来源

ThreadLocal:线程中的全局变量的更多相关文章

  1. python线程中的全局变量与局部变量

    在python多线程开发中,全局变量是多个线程共享的数据,局部变量是各自线程的,非共享的. 如下几种写法都是可以的: 第一种:将列表当成参数传递给线程 from threading import Th ...

  2. java中的全局变量如何实现?ThreadLocal~

    全局变量就是不管你在哪里,都能够直接引用的变量,还不用担心各种问题.每个语言都有自己的全局变量,我想! 一般地,面向过程的语言当中,可能就是一个声明在最前面的变量,后面的代码直接引用,就成了全局变量! ...

  3. 参数在一个线程中各个函数之间互相传递的问题(ThreadLocal)

    ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源. 一个ThreadLocal变量虽然是 ...

  4. python 线程中的局部变量ThreadLocal

    一个线程使用自己的局部变量比使用全局变量好局部变量只有线程自己能看见,不会影响其他线程全局变量的修改必须加锁 ThreadLocal 线程局部变量 import threading # 创建全局Thr ...

  5. (未使用AOP)使用ThreadLocal对象把Connection和当前线程绑定, 从而使一个线程中只有一个能控制事务的对象

    每个连接都有自己的独立事务,会造成数据的不一致 这组操作应该要么一起操作成功,要么一起操作失败, 应该使用同一个连接,只有一个能控制事务的对象 需要使用ThreadLocal对象把Connection ...

  6. ThreadLocal(在一个线程中共享数据)

    ThreadLocal 在"事务传递Connection"参数案例中,我们必须传递Connection对象,才可以完成整个事务操作.如果不传递参数,是否可以完成?在JDK中给我们提 ...

  7. java 多线程 :ThreadLocal 共享变量多线程不同值方案;InheritableThreadLocal变量子线程中自定义值,孙线程可继承

      ThreadLocal类的使用 变量值的共享可以使用public static变量的形式,所有的线程都是用同一个public static变量.如果想实现每一个线程都有自己的值.该变量可通过Thr ...

  8. Python并发编程之谈谈线程中的“锁机制”(三)

    大家好,并发编程 进入第三篇. 今天我们来讲讲,线程里的锁机制. 本文目录 何为Lock( 锁 )?如何使用Lock( 锁 )?为何要使用锁?可重入锁(RLock)防止死锁的加锁机制饱受争议的GIL( ...

  9. python线程中的同步问题

    多线程开发可能遇到的问题 假设两个线程t1和t2都要对num=0进行增1运算,t1和t2都各对num修改1000000次,num的最终的结果应该为2000000.但是由于是多线程访问,有可能出现下面情 ...

  10. ThreadLocal线程范围内的共享变量

    模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程. package com.ljq.test.thread; import java.util.Has ...

随机推荐

  1. upload-labs 第一关 前端验证绕过!

    打开靶场发现只能上传jpg png gif 的文件格式的文件,我们想要上传上去的文件格式为php文件格式,首先在Notepad++里面打开图片,会出现很多乱码,我们在最后面添加漏洞语句<?php ...

  2. Python基础 - 标识符命名规范

    标识符是什么? 标识符主要用来给变量名,函数名,方法名,类名起名时要遵循的规范 硬性规则   见名知意(使用中文转译后的英文)  由字母,数字,下划线组成, 并且不能以数字开头, 不能和Python关 ...

  3. 深度解析 slab 内存池回收内存以及销毁全流程

    在上篇文章 <深入理解 slab cache 内存分配全链路实现> 中,笔者详细地为大家介绍了 slab cache 进行内存分配的整个链路实现,本文我们就来到了 slab cache 最 ...

  4. 代码随想录算法训练营Day31 贪心算法| 122.买卖股票的最佳时机II 55. 跳跃游戏 45.跳跃游戏II

    代码随想录算法训练营 122.买卖股票的最佳时机II 题目链接:122.买卖股票的最佳时机II 给定一个数组,它的第 i个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润. ...

  5. django:有关移除数据库出错问题

    执行: 最终,通过执行迁移文件,我们将Django项目中创建的模型转化为MySql中的数据表. 执行迁移文件的两条命令: python manage.py makemigrations python ...

  6. Mybatis的parameterType造成线程阻塞问题分析

    一.前言 最近在新发布某个项目上线时,每次重启都会收到机器的 CPU 使用率告警,查看对应监控,持续时长达 5 分钟,对于服务重启有很大风险.而该项目有非常多 Consumer 消费,服务启动后会有大 ...

  7. 深入探究for...range语句

    1. 引言 在Go语言中,我们经常需要对数据集合进行遍历操作.对于数组来说,使用for语句可以很方便地完成遍历.然而,当我们面对其他数据类型,如map.string 和 channel 时,使用普通的 ...

  8. 整理spring-web里支持的文件以及对应的Content-Type

    前言 最近在弄文件上传.下载.在线预览时经常需要设置请求标头或者响应标头的Content-Type 属性.所以研究了一下spring支持哪些Content-Type,通过研究MediaTypeFact ...

  9. R语言中的跨平台支持:如何在Windows、MacOS和Linux上使用R语言进行数据分析和可视化

    目录 当今数据科学领域,R语言已经成为了数据分析和可视化的流行工具.R语言具有强大的功能和灵活性,使得它可以在各种不同的平台上运行,包括Windows.MacOS和Linux.因此,本文将介绍R语言中 ...

  10. React学习时,自己拟定的一则小案例(table表格组件,含编辑)

    某次在Uniapp群看到有人问uniapp如何操作dom元素. 他想对这张表标红的区域,做dom元素获取,因为产品想让红色色块点击时,成为可编辑,渲染1~4月份之间的行程安排. 于是,有小伙伴说让他用 ...