多线程-线程本地变量ThreadLocal

embedded/2025/3/6 7:07:20/

简介

ThreadLocal是线程本地变量,用于存储独属于线程的变量,这些变量可以在同一个线程内跨方法、跨类传递。每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal实例

常用操作:ThreadLocal有set、get、remove三个动作,分别对应存储、获取、清除

ThreadLocal的实际应用:

  • 案例1:jdbc中使用ThreadLocal来存储数据库连接,从而实现事务控制的功能。ThreadLocal可以保证同一个线程内获取到的数据库连接是同一个,用户可以在这个连接下开启事务、执行sql、提交事务。
  • 案例2:在某些业务系统中,会使用ThreadLocal来存储发起当前请求的用户的信息,当请求来临时,把用户信息放到ThreadLocal中,处理完请求后,清理ThreadLocal中的用户信息,这样,在处理请求时,在业务代码中就可以通过ThreadLocal来获取用户信息。

ThreadLocal的运行机制:

  • 向ThreadLocal中设置元素时,ThreadLocal会获取当前线程对象中的ThreadLocalMap实例,
    • 如果可以获取到,ThreadLocal会把自己作为键,set方法的参数作为值,存储到ThreadLocalMap中,
    • 如果获取不到,就创建一个ThreadLocalMap

使用案例

案例1:基本使用

基本使用,这里演示了ThreadLocal的set、get、remove

java">private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();public static void main(String[] args) {// 向threadLocal中存入变量LOCAL.set("aaa");// 获取变量System.out.println("local.get() = " + LOCAL.get()); // aaa// 移除变量LOCAL.remove();System.out.println("local.get() = " + LOCAL.get()); // null
}

案例2:多个线程操作同一个ThreadLocal

多个线程操作同一个ThreadLocal实例,同时向里面设置元素、获取元素,验证会不会出现线程安全问题?

java">private static final ThreadLocal<Object> LOCAL_VAR = new ThreadLocal<>();public static void main(String[] args) {// 线程1new Thread(() -> {LOCAL_VAR.set(Thread.currentThread().getName());Utils.println(LOCAL_VAR.get());  // t1LOCAL_VAR.remove();Utils.println("after remove : " + LOCAL_VAR.get()); // null},"t1").start();// 线程2new Thread(() -> {LOCAL_VAR.set(Thread.currentThread().getName());Utils.println(LOCAL_VAR.get());  // t2LOCAL_VAR.remove();Utils.println("after remove : " + LOCAL_VAR.get()); // null},"t2").start();
}

总结:不会出现线程安全问题。原因随后讲

案例3:一个线程操作多个ThreadLocal

java">private static final ThreadLocal<Object> LOCAL_VAR = new ThreadLocal<>();
private static final ThreadLocal<Object> LOCAL_VAR_2 = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {// 设置并打印本地变量LOCAL_VAR.set("aa");LOCAL_VAR_2.set("bb");Utils.println(LOCAL_VAR.get()); // aaUtils.println(LOCAL_VAR_2.get()); // bb// 清除本地内存中的本地变量LOCAL_VAR.remove();LOCAL_VAR_2.remove();},"t1").start();
}

总结:如果线程需要n个本地局部变量,那么它就需要使用n个ThreadLocal实例,一个ThreadLocal实例对应当前线程中的一个本地变量

案例4:使用ThreadLocal,实现线程间的数据隔离

java1.1提供的日期类是线程不安全的,这里使用ThreadLocal来解决线程安全的问题

java">public static void main(String[] args) {ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<>();for(int i = 0; i < 10; i++) {new Thread(new Runnable() {@Overridepublic void run() {// 创建一个格式化日期的实例,然后存放到ThreadLocal中dateFormatThreadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));Date date = null;try {date = dateFormatThreadLocal.get().parse("2021-08-15 12:08:09");} catch (ParseException e) {e.printStackTrace();}Utils.println("date = " + date);dateFormatThreadLocal.remove();}}).start();}
}

总结:SimpleDateFormat本身是线程不安全的,在这个案例中,每个线程都会创建一个SimpleDateFormat实例,然后存入到ThreadLocal中,使用时再从ThreadLocal中取出,避免多个线程复用同一个SimpleDateFormat实例,从而实现线程安全。在实际使用中,这也是一种解决问题的方式。但是,ThreadLocal最常见的操作还是用于在同一个线程内跨方法、跨类传递变量,通常不会考虑用ThreadLocal来实现线程安全,因为这样做消耗也会成倍增加,例如在本案例中,有几个线程,就需要几个SimpleDateFormat实例。常见的策略是,考虑把多线程共享的实例做成无状态的,只负责计算,不存储数据,如果确实需要访问共享资源,可以使用锁。

源码分析

ThreadLocal的类结构

java">public class ThreadLocal<T> {  // 泛型T,是ThreadLocal中存储的元素的数据类型// 静态内部类ThreadLocalMapstatic class ThreadLocalMap {// 静态内部类中还有一个静态内部类 Entry,注意,这里Entry继承了WeakReference,它代表弱引用static class Entry extends WeakReference<ThreadLocal<?>> {// 这里的value就是ThreadLocal调用set时存入的值Object value;Entry(ThreadLocal<?> k, Object v) {super(k);  // key是弱引用value = v;}}// 存储变量的数据结构private Entry[] table;private int size = 0;private int threshold;}
}// Thread类中持有ThreadLocalMap的引用,这就是线程局部变量存放的地方
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;
}

总结:这里最关键的是ThreadLocalMap,它被声明在ThreadLocal中,同时,Thread持有ThreadLocalMap的实例,这个实例中存放的就是线程本地变量。而且要注意,ThreadLocalMap中的Entry,它用于包装键值对,键是弱引用,随后会学习到。

set方法

用户调用set方法,向当前线程存入一个局部变量

java">// ThreadLocal中的set方法
public void set(T value) {// 获取当前线程对象Thread t = Thread.currentThread();// 获取线程对象内ThreadLocalMap类型的成员变量ThreadLocalMap map = getMap(t);// 如果map不等于null,以当前threadLocal对象作为键,把用户指定的值设置到map中if (map != null)map.set(this, value);else// 创建map对象,设置键值对createMap(t, value);
}

最终会把变量设置到ThreadLocalMap中,注意,在这个map中,key是ThreadLocal,值是set方法的参数

get方法

获取ThreadLocal在当前线程实例中对应的值

java">public T get() {// 获取当前线程对象Thread t = Thread.currentThread();// 获取线程对象内ThreadLocalMap类型的成员变量ThreadLocalMap map = getMap(t);// 如果map不等于nullif (map != null) {// 以当前threadLocal对象作为键,获取键值对ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {// 获取键值对中的值@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 如果map等于null,这里是将键置为当前对象,值置为null,然后返回null值return setInitialValue();
}

同样,这里是从Thread实例中获取ThreadLocalMap的实例,然后使用ThreadLocal作为key,从ThreadLocalMap中获取value,remove方法也类似

ThreadLocalMap

ThreadLocalMap,顾名思义,它是一个map,存储键值对,在之前学习set、get方法时,发现所有的变量最终都会存储到其中,并且key是ThreadLoal。在这里深入了解ThreadLocalMap是如何存储数据的。

ThreadLocalMap类似于HashMap,底层是数组,根据key的哈希值,计算下标,然后把元素存放到下标处,和HashMap不同的在于,如果有冲突,它没有采用链地址法来处理冲突,而是把元素直接存放到 下标 + 1 的位置,如果 下标 + 1 = 数组长度,直接把元素存入第0位,然后一直循环,直到找到空位。

ThreadLocalMap的成员变量:

java">static class ThreadLocalMap {private Entry[] table;  // 存储元素的底层数组private int size = 0;   // 元素个数private static final int INITIAL_CAPACITY = 16;  // 数组的初始容量private int threshold;  // 阈值,决定何时扩容// 构造方法,构造方法的参数是第一个键值对,会把它们存入数组中。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);}}

存放键值对的方法:set方法(这里是ThreadLocalMap中的set方法,不是ThreadLocal中的)

java">// 参数是键值对,注意,键是ThreadLocal
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;// 计算下标,哈希值和 数组长度 - 1 按位与,了解HashMap的这招都不陌生,// 数组长度要求必须是2的次方,按位与的结果是0到数组长度减1int i = key.threadLocalHashCode & (len-1);// for循环,如果进入循环,表示下标处已经有值,就需要处理哈希冲突for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 如果新增的key和原先的key一样,更新valueif (k == key) {e.value = value;return;}// 如果key是null,清理value。这里证明发生了内存泄漏,// 因为key都没有了,但是值还在,所以需要清理value,随后详细讲解。if (k == null) {replaceStaleEntry(key, value, i);return;}}// 循环结束,证明找到了可以存放元素位置,存放元素tab[i] = new Entry(key, value);int sz = ++size;// 如果元素个数大于阈值,扩容if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}

根据键获取值的方法:getEntry方法

java">private Entry getEntry(ThreadLocal<?> key) {// 计算下标,获取元素int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)// 如果键值对中键是匹配的,返回键值对return e;else// 如果没有找到元素,可能发生了哈希冲突,需要继续寻找return getEntryAfterMiss(key, i, e);
}// TgetEntryAfterMiss方法,处理哈希冲突
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;// 如果键等于null,将value置为null,同样涉及到内存泄漏if (k == null)expungeStaleEntry(i);else// 下一个下标i = nextIndex(i, len);e = tab[i];}return null;
}

总结:这一节描述了ThreadLocalMap是如何存储和获取元素的,和普通的map基本类似,只不过它使用了再寻址法来处理哈希冲突。

ThreadLocalMap的工作机制:在学习了一部分ThreadLocal的源码之后,这里用文字和图片总结一下ThreadLocal的工作机制

工作机制:

  • ThreadLocal内部定义了ThreadLocalMap
  • Thread类持有ThreadLocalMap的实例
  • ThreadLocalMap中维护了一个数组,数组中的每一个元素都是一个键值对,键是当前ThreadLocal实例,值是用户设置的值。
  • 同一个线程操作的多个ThreadLocal实例,都存储在一个ThreadLocalMap中
  • 多个线程操作同一个ThreadLocal实例,每个线程都有自己的ThreadLocalMap,key是同一个,但map有多个,key对应的值分别存储在不同的map中。

时序图:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159在这里插入图片描述
png?origin_url=.%2Fimage%2F3.%E5%A4%9A%E7%BA%BF%E7%A8%8B-%E7%BA%BF%E7%A8%8B%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%2Fimage-20250227173936522.png&pos_id=img-NJ9FX13n-1741182359260)

ThreadLocal的内存泄漏

ThreadLocal内存泄露指的是,虽然ThreadLocalMap中的键被回收了,但是值还在

发生内存泄漏的原因:

  1. 用户在自己编写的代码中声明的ThreadLocal对象,本身是一个强引用,这个ThreadLocal对象同时会作为ThreadLocalMap中的键,这个键是一个弱引用。
  2. 只要用户代码中ThreadLocal类型的对象没被回收,它的值不等于null,那ThreadLocalMap中的key就不会在GC时被回收。
  3. 如果用户代码中ThreadLocal类型的对象被置为null,ThreadLocalMap中的key会在GC时被回收,如果此前没有移除value,那么这个value就会无法被获取到
  4. ThreadLocalMap是依附在Thread上的,只要Thread销毁,那ThreadLocalMap也会销毁,但是如果线程一直存在,value一直无法回收,就会造成内存泄漏

为什么要将ThreadLocalMap的key设置为弱引用呢?外界是通过ThreadLocal来对ThreadLocalMap进行操作的,假设外界使用ThreadLocal的对象被置null了,那ThreadLocalMap的强引用指向ThreadLocal也毫无意义,弱引用反而可以预防大多数内存泄漏的情况,毕竟被回收后,下一次调用set/get/remove时ThreadLocal内部会清除掉value

ThreadLocal针对内存泄漏做的保护措施:如果在操作ThreadLocal时,发现key为null,会将value清除掉,同时会遍历map,寻找是否还有这样的值

存在长期性内存泄露需要满足条件:ThreadLocal被回收、线程被复用、线程复用后不再调用ThreadLocal的set/get/remove方法

如何避免:一个set对应一个remove,一定要确保元素在不用之后被移除。


http://www.ppmy.cn/embedded/170422.html

相关文章

Tick数据20241224

Tick数据20241224 商品和金融期货level2高频数据&#xff08;一秒四次&#xff09;下载 链接: https://pan.baidu.com/s/144ewl4T0dQvrAedhLz8uJw?pwdc33h 提取码: c33h通过历史Level2一秒四次高频数据深层次分析交易可以分析出比较活跃的品种一&#xff1a;m2505 (1)在11:1…

机器学习—赵卫东阅读笔记(一)

第一章&#xff1a;机器学习基础 1.1.2 机器学习主要流派 1.符号主义 2.贝叶斯分类——基础是贝叶斯定理 3.联结主义——源于神经学&#xff0c;主要算法是神经网络。——BP算法&#xff1a;作为一种监督学习算法&#xff0c;训练神经网络时通过不断反馈当前网络计算结果与…

物联网感应层设备的通信协议及数据上传路径详解

以下是物联网感应层设备的通信协议及数据上传路径详解&#xff0c;包含典型技术方案和实际应用案例&#xff1a; 一、通信协议矩阵 短距离传输&#xff08;<100m&#xff09; 协议类型技术特性典型设备应用场景BLE 5.22Mbps速率&#xff0c;mesh组网可穿戴设备医疗手环连接…

剑指 Offer II 059. 数据流的第 K 大数值

comments: true edit_url: https://github.com/doocs/leetcode/edit/main/lcof2/%E5%89%91%E6%8C%87%20Offer%20II%20059.%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%9A%84%E7%AC%AC%20K%20%E5%A4%A7%E6%95%B0%E5%80%BC/README.md 剑指 Offer II 059. 数据流的第 K 大数值 题目描述 设…

JavaWeb XML

1、定义 EXtension markup language XML&#xff1a;可扩展自定义标记语言 2、XML的存在意义和用法 XML存在约束&#xff0c;可以自定义但也存在书写规则&#xff0c;一般不需要逐行书写。 我们使用XML&#xff0c;只需要基于第三方应用程序和已提供框架的配置文件进行修改…

SpringBoot项目集成ElasticSearch

1. 项目背景 处于失业找工作的阶段&#xff0c;随便写写吧~ 没啥背景&#xff0c;没啥意义&#xff0c;Java后端越来越卷了。第一学历不是本科&#xff0c;感觉真的是没有一点路可走。 如果有路过的小伙伴&#xff0c;如果身边还有坑位&#xff0c;不限第一学历的话&#xff0…

leetcode每日一题——1328. 破坏回文串

给你一个由小写英文字母组成的回文字符串 palindrome &#xff0c;请你将其中 一个 字符用任意小写英文字母替换&#xff0c;使得结果字符串的 字典序最小 &#xff0c;且 不是 回文串。 请你返回结果字符串。如果无法做到&#xff0c;则返回一个 空串 。 如果两个字符串长度…

leetcode1 两数之和 哈希表

什么时候使用哈希法&#xff0c;当我们需要查询一个元素是否出现过&#xff0c;或者一个元素是否在集合里的时候&#xff0c;就要第一时间想到哈希法。 242. 有效的字母异位词 (opens new window)这道题目是用数组作为哈希表来解决哈希问题&#xff0c;349. 两个数组的交集 (o…