Java并发编程第8讲——ThreadLocal详解

news2025/2/26 8:10:46

ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。

一、什么是ThreadLocal

ThreadLocal是java.lang下面的一个类,在JDK 1.2版本加入,作者是Josh Bloch(集合大神)和Doug Lea(并发大神)

它提供了一种线程局部变量的方式,线程局部变量是指每个线程都拥有自己独立的变量副本,互不干扰,通过ThreadLocal,可以方便地在多线程环境下共享数据,同时不需要考虑线程安全性,这也是解决并发问题的途径之一。

例如:在web开发中,可以使用ThreadLocal来保存用户的登录信息,以便每个线程都能够独立地获取和修改自己的登录信息,避免了线程之间的干扰。

二、ThreadLocal的使用

ThreadLocal有四个方法,分别为:

  • protected T initialValue():返回此线程局部变量的初始值。

  • pubulic T get(): 返回当前线程局部变量的当前线程副本的值。如果这是线程第一次调用该方法,则创建并初始化此副本。

  • public void set(T value):将此线程局部变量的当前线程的副本设置为指定的值。

  • public void remove():移除此线程局部变量的当前线程的值。

下面使用ThreadLocal来模拟用户登录信息的场景:

ThreadLocal工具类:

public class CurrentUserHolder {
    public static ThreadLocal<User> threadLocal=new ThreadLocal<>();
​
    public static void setUser(User user){
        threadLocal.set(user);
    }
​
    public static User getUser(){
        if (Objects.nonNull(threadLocal.get())) {
            return threadLocal.get();
        }
        throw new RuntimeException("当前用户信息为空!");
    }
​
    public static void clearUser(){
        threadLocal.remove();
    }
}

User实体类:

@Data
public class User {
    private String name;
    private Integer age;
}

测试:

public class Test {
    public static void main(String[] args) {
        //用户登录
        User user = new User();
        user.setName("小黑子");
        user.setAge(18);
        //将用户信息保存在ThreadLocal中
        CurrentUserHolder.setUser(user);
        //在其它方法中,可以通过ThreadLocal获取用户信息
        User localUser = CurrentUserHolder.getUser();
        System.out.println(localUser);//输出:User(name=小黑子, age=18)
        //用户操作完成后,可以remove掉
        CurrentUserHolder.clearUser();
    }
}

ps:由于ThreadLocal是基于线程的,所以在不同的线程中,通过ThreadLocal获取的用户信息是独立的,这在多线程环境下非常有用,可以避免线程之间的数据混乱和冲突。

三、ThreadLocal的实现原理

直接上图!下图中基本描述出了ThreadThreadLocalMapThreadLocal三者之间的关系。

解释一下:

  • ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构,key就是当前ThreadLocal对象,value就是我们要保存的值。

  • Thread类中维护了两个ThreadLocalMap成员变量,threadLocals和inheritableThreadLocals,它们的默认值是null,类型为ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap,感兴趣的可以去看一下源码。

四、核心源码

4.1 ThreadLocalMap内部类

在静态内部类ThreadLocalMap中,维护了一个数据结构类型为Entry的数组,源码如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
   Object value;
   Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
}

从源码中我们可以看到,Entry继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型(也就是我们需要保存的值)

我们再来看一下它的成员变量:

//数组的默认初始化容量
private static final int INITIAL_CAPACITY = 16;
//Entry数组,大小必须为2的幂
private Entry[] table;
//数组内部元素个数
private int size = 0;
//数组扩容阈值,默认为0,创建ThreadLocalMap对象后会被重新设置
private int threshold; 

是不是有点熟悉,这几个变量和HashMap中的变量很类似,功能也类似。

最后看一下它的构造方法:

/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
   table = new Entry[INITIAL_CAPACITY];
   int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
   table[i] = new Entry(firstKey, firstValue);
   size = 1;
   setThreshold(INITIAL_CAPACITY);
}

注释翻译过来大概就是,该构造方法是懒加载的,只有我们创建一个Entry对象并需要放入到Entry数组的时候才会去初始化数组。

4.2 set()方法

接下来我们就介绍一下ThreadLocal常用的一些方法吧,首先看一下set()方法:

public void set(T value) {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取当前线程的ThreadLocalMap对象
  ThreadLocalMap map = getMap(t);
  if (map != null)
      // 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中
      map.set(this, value);
   else
       // 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value
      createMap(t, value);
}

解释:

  • 获取当前线程,拿到当前Thread的ThreadLocalMap对象。

  • 如果map存在,则将当前ThreadLocal对象作为key,value作为value放入map中。

  • 如果map不存在,则创建一个新的ThreadLocalMap对象,并新建一个Entry放入该ThreadLocalMap, 调用set方法的ThreadLocal和传入的value作为该Entry的key和value。

4.3 get()方法

源码如下:

public T get() {
  //获取当前线程
  Thread t = Thread.currentThread();
  //获取当前线程的ThreadLocalMap对象
  ThreadLocalMap map = getMap(t);
  if (map != null) {
  //map存在,通过this(当前ThreadLocal)获取Entry
     ThreadLocalMap.Entry e = map.getEntry(this);
     if (e != null) {
          @SuppressWarnings("unchecked")
         //Entry不为空,返回该Entry的value值
          T result = (T)e.value;
          return result;
     }
 }
   //map不存在,调用setInitialValue()方法设置初始值
   return setInitialValue();
}

解释:

  • 通过当前线程获取ThreadLocalMap:

    • 如果map存在,则通过当前ThreadLocal获取对应的Entry,若Entry不为空,返回该Entry的value值。

    • 如果map不存在,则调用setInitialValue()方法设置初始值。

  • setInitialValue():

    • 根据initalValue()方法获取value值,默认值为null,可以重写该方法。

    • 通过当前线程获取ThreadLocalMap对象。

    • map存在,设置当前值为上述value,不存在则创建新的ThreadLocalMap,并将值设置为value。

4.4 remove()方法

源码如下:

public void remove() {
  //根据当前线程获取ThreadLocalMap对象
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
      //存在,执行remove方法
     m.remove(this);
}

解释:

  • 根据当前线程获取ThreadLocalMap对象,存在则执行remove()方法。remove(this)方法中,将ThreadLocal作为key来删除对应的Entry。

五、内存泄漏问题

5.1 分析

读到这,相信你对ThreadLocal的基本原理有了更深一步的理解,我们把上图补全,从堆栈视角看一下它们之间的引用关系。

我们可以看到,ThreadLocal对象,有两个引用,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中Key对它的引用。如果栈上的ThreadLocal引用不再使用了,那么ThreadLocal对象因为还有一条引用链在,所以会导致它无法回收,久而久之就会OOM。

这就是我们所说的ThreadLocal的内存泄漏问题,为了解决这个问题,ThreadLocalMap使用了弱引用,就是上述我们说过的Entry数组:

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
   Object value;
   Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
}

可以看出,ThreadLocal的引用k通过构造方法传递给了Entry类的父类WeakReference的构造方法,那么可以理解为ThreadLocalMap中的键是ThreadLocal的弱引用。

穿插一下Java中的四大引用:

  • 强引用:Java中默认的引用类型,只要引用还存在,即便OOM也不会被回收。

  • 软引用:内存不足时,将会被干掉。

  • 弱引用:无论内存充足与否,只要执行GC,就会被干掉。

  • 虚引用:最弱的一种引用,存在意义就是为了将关联虚引用的对象在被GC掉之后收到一个通知。

如果用了弱引用,那么ThreadLocal对象就可以在下次GC的时候被回收掉了。

这样做可以很大程度上避免了因为ThreadLocal的使用而导致的OOM问题,但也无法彻底避免

我们可以看到,虽然key是弱引用,但是value是强引用,而且它的生命周期是和Thread一样的,也就是说,只要Thread还在,那么这个对象就无法被回收。

那么,什么情况下,Thread会一直在呢,那就是线程池,这就导致value一直无法被回收。

5.2 如何解决

ThreadLocalMap底层使用数组来保存元素,使用“线性探测法”来解决hash冲突,在每次调用ThreadLocal的get、set、remove方法时,内部会实际调用ThreadLocalMap的get、set、remove等操作,而ThreaLocalMap的每次set、get、remove时,都会对key为null的Entry进行清除(expungeStateEntry()方法,将Entry的value清空,等下次GC就会被回收)。

所以,当我们一个ThreadLocal用完后,就手动remove一下,就可以在下次GC时,把Entry清理掉。

5.3 总结

上述我们分了两种情况来看ThreadLocal内存泄漏问题:

  • key使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLocalMap持有ThreadLocal的强引用,如果没有手动remove,ThreadLocal不会被回收,导致Entry内存泄漏。

  • key使用弱引用:引用ThreadLocal被回收,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动remove,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动remove,就会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动remove就会导致内存泄漏,而不是因为弱引用。

End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

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

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

相关文章

植隆业务中台与金蝶云星空对接集成服务工单查询接口连通应收单新增(6202-开票申请(代理商-销售类))

植隆业务中台与金蝶云星空对接集成服务工单查询接口连通应收单新增(6202-开票申请&#xff08;代理商-销售类&#xff09;) 数据源系统:植隆业务中台 承载了企业核心关键业务&#xff0c;是企业的核心业务能力&#xff0c;也是企业数字化转型的重点。业务中台的建设目标是&…

网络爬虫-----http和https的请求与响应原理

目录 前言 简介 HTTP的请求与响应 浏览器发送HTTP请求的过程&#xff1a; HTTP请求主要分为Get和Post两种方法 查看网页请求 常用的请求报头 1. Host (主机和端口号) 2. Connection (链接类型) 3. Upgrade-Insecure-Requests (升级为HTTPS请求) 4. User-Agent (浏览…

原生js之script基本属性

async:异步执行脚本 defer:延迟脚本下载 src:要执行的代码外部文件地址 noscript:表示浏览器不支持或拒绝支持script脚本时出现的内容 async和defer async和defer本质都是为了让脚本推迟到整个页面解析后再下载&#xff0c;不同的是async是异步无序的&#xff0c;而defer是同…

基于微信小程序的在线小说阅读系统,附数据库、教程

1 功能简介 Java基于微信小程序的在线小说阅读系统 微信小程序的在线小说阅读系统&#xff0c;系统的整体功能需求分为两部分&#xff0c;第一部分主要是后台的功能&#xff0c;后台功能主要有小说信息管理、注册用户管理、系统系统等功能。微信小程序主要分为首页、分类和我的…

【开发】视频监控系统/视频汇聚平台EasyCVR对国标类型编码进行判断的实现方式

视频监控平台/视频存储/视频分析平台EasyCVR基于云边端一体化管理&#xff0c;支持多类型设备、多协议方式接入&#xff0c;具体包括&#xff1a;国标GB28181协议、RTMP、RTSP/Onvif、海康Ehome&#xff0c;以及海康SDK、大华SDK、华为SDK、宇视SDK、乐橙SDK、萤石SDK等&#x…

利用免费的敏捷研发管理工具管理端到端敏捷研发流程

Leangoo领歌是Scrum中文网&#xff08;scrum.cn&#xff09;旗下的一款永久免费的敏捷研发管理工具。 Leangoo领歌覆盖了敏捷研发全流程&#xff0c;它提供端到端敏捷研发管理解决方案&#xff0c;包括小型团队敏捷开发&#xff0c;规模化敏捷SAFe&#xff0c;Scrum of Scrums…

进一步观察扩散模型中的参数有效调整

摘要&#xff1a; 像Stable diffusion[31]这样的大规模扩散模型非常强大&#xff0c;可以找到各种真实世界的应用程序&#xff0c;而通过微调来定制这样的模型会降低内存和时间的效率。受自然语言处理最新进展的推动&#xff0c;我们通过插入小型可学习模块adapters(称为适配器…

链表实现稀疏多项式相加(C++)

#include<iostream> using namespace std; typedef struct node {float coef;//系数int expn;//指数struct node* next; }list,*linklist; void Createlist(linklist& l) {l new list;l->next NULL;linklist p,q;q l;cout << "输入多项式项数&#…

ARM 汇编指令集——汇编中三种符号(汇编指令、伪指令、伪操作)、汇编基本格式、数据操作指令、跳转指令、特殊功能寄存器操作指令、内存操作指令、混合编程

目录 一、汇编中三种符号&#xff08;汇编指令、伪指令、伪操作&#xff09; 二、汇编基本格式 三、数据操作指令 3.1 数据搬移指令mov/mvn ① 示例 ② 立即数 3.2 移位操作指令lsl/lsr/asr/ror 示例 3.3 位运算操作指令and/orr/eor/bic ① 示例1 ② 示例2 3.4 算数…

第32节——useReducer——了解

一、概念 useReducer 是在 react V 16.8 推出的钩子函数&#xff0c;从用法层面来说是可以代替useState。众所周知&#xff0c;useState 常用在单个组件中进行状态管理&#xff0c;但是遇到状态全局管理的时候&#xff0c;useState 显然不能满足我们的需求&#xff0c;这个时候…

4G工业路由器高效数据传输助力光伏发电站管理

光伏发电站是能源产业中一种利用太阳能技术将光转化为电能的常见设施。随着物联网技术与环保能源的不断进步和应用的普及&#xff0c;光伏发电站的管理也变得更加便捷高效。 光伏发电站结合4G工业路由器实现远程监控管理&#xff0c;并用于采集发电站中的传感器数据和监控信息…

vue watch 侦听器 监视器

vue watch 侦听器 监视器 变量 变化的时候&#xff0c;自动调用处理函数 vue watch 侦听器 监视器

/node_modules/XXX/index.js:XXX XXX ??= X;SyntaxError: Unexpected token ‘??=‘

这问题 老实说有点奇葩 不影响运行 反倒运行提交了 不解决这个问题提交不了代码 这个错误是由于语法不兼容导致的。?? 是一个相对较新的 JavaScript 语法&#xff0c;也就是空值合并赋值操作符&#xff0c;它在 Node.js 版本低于 15 或者某些浏览器中不被支持。 那么 了解…

日常生活中的常用命令及操作

目录 一、Windows11 中查看网卡名称 及ip地址 二、查看硬件的详细信息 三、查看显卡声卡详细信息及厂商 四、C盘清理 第一步 输入wini 开启Windows设置主界面 第二步 存储中还有一个叫存储感知的功能 第三步 更改新内容的保存位置 第四步 怕误C盘内的东西可以 查看详细的…

【Java 基础篇】Java 线程通信详解

多线程编程在实际应用中非常常见&#xff0c;但随之而来的问题是线程之间的通信。线程通信是多线程编程中一个至关重要的概念&#xff0c;它涉及到线程之间的信息传递、同步和协作。本篇博客将详细解释Java中的线程通信&#xff0c;包括什么是线程通信、为什么需要线程通信、如…

UG\NX二次开发 求空间点坐标按照某方向移动某距离后的新坐标

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 简介: 群友问“ufun中空间点的坐标(x,y,z),沿着空间任意工作坐标系z轴移动10mm后的新坐标如何计算?” 这个是有多种方法的,比如数学计算比如ufun的仿射变换…

css,环形

思路&#xff1a; 1.先利用conic-gradient属性画一个圆&#xff0c;然后再叠加 效果图 <template><div class"ring"><div class"content"><slot></slot></div></div> </template> <script> import …

JavaScript系列从入门到精通系列第三篇:JavaScript基本语法(一)

文章目录 一&#xff1a;JavaScript基本语法 1&#xff1a;JS注释 (一)&#xff1a;JS多行注释 (二)&#xff1a;JS单行注释 (三)&#xff1a;JS中大小写 (四)&#xff1a;分号问题 (五)&#xff1a;空格和换行 2&#xff1a;字面量和变量 (一)&#xff1a;字面量 (二…

demo1-csa(从初阶到大牛)

1.1文件管理命令练习 (1) 在/opt目录下创建一个临时目录tmp; # 创建临时目录tmp sudo mkdir /opt/tmp (2) 在临时目录下创建一个文件&#xff0c;文件名为a.txt; # 在临时目录下创建文件a.txt&#xff08;可以使用touch命令创建空文件&#xff09; sudo touch /opt/tmp/a.t…

MQ - 11 Kafka的架构设计与实现

文章目录 导图概述Kafka 系统架构协议和网络模块数据存储元数据存储消息数据生产者和消费者生产者消费者HTTP 协议支持和管控操作Kafka 从生产到消费的全过程总结导图 概述 在学习的过程中,我们会发现 Kafka 和 RocketMQ 的架构是非常像的,那为什么还要单独来分析 Kafka 呢?…