ThreadLocal解惑

embedded/2024/9/23 6:26:08/

目录

ThreadLocal%E6%98%AF%E4%BB%80%E4%B9%88%3F-toc" style="margin-left:0px;">1、ThreadLocal是什么?

ThreadLocal%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86-toc" style="margin-left:0px;">2、ThreadLocal实现原理

3、设置线程变量的2种方式

ThreadLocal%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98-toc" style="margin-left:0px;">4、关于ThreadLocal的内存泄漏问题

5、使用过程中的注意事项和误区


1、ThreadLocal是什么?

    比较书面的回答:
类如其名,线程本地变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。这句话没问题,但容易被人误解,会被误以为:任意变量用ThreadLocal维护都是线程隔离的。后面会解答这个问题。

ThreadLocal%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86">2、ThreadLocal实现原理

       每个线程(Thread)中都有一个 ThreadLocalMap容器,通过ThreadLocal可以存放和读取Thread中的ThreadLocalMap。每个Thread对象之间是隔离的,Thread对象中的ThreadLocalMap容器自然也是隔离的。
通俗点来说:可以把ThreadLocal看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap,实现互不干扰。

要探究原里,就离不开两个类:Thread和ThreadLocal,先分别看一下这两个类,为了方便理解,这里就简要介绍核心的概念和源码,更细节的东西查看源码就行,源码不多也很简单。

1)关于Thread类

如下代码判断,代码中的任意位置我们都可以通过 Thread.currentThread() 来获取当前线程对象,即可以获得当前线程的名称、id等等属性;但是无法直接获取到Thread中的ThreadLocalMap。

java">    public static void main(String[] args) {// 获取当前线程-主线程Thread mainThread = Thread.currentThread();System.out.println("main thread id: " + mainThread.getId());System.out.println("main thread name: " + mainThread.getName());System.out.println("------------------------------");// 自定义线程 1Thread thread1 = new Thread(()-> {// 获取当前线程Thread th = Thread.currentThread();System.out.println("thread id: " + th.getId());System.out.println("thread name: " + th.getName());});// 设置线程名称thread1.setName("MyThread-1");thread1.start();}

执行结果:

看Thread类的源码,里面有一个属性ThreadLocalMap,该Map就是用来存储各线程独立变量的。

2) 关于ThreadLocal
    前面说了,可以把它看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap。

简单看一下ThreadLocal类的源码

Thread类中的ThreadLocalMap属性是ThreadLocal类中的内部类

从源码可以看出,ThreadLocal类中有内部类ThreadLocalMap,ThreadLocalMap中有内部类Entry,Entry类有两个属性,k和v。ThreadLocalMap是用的Entry数组来存储数据(Entry对象)。

使用ThreadLocal的set方法添加一个变量,下面通过代码来看一下这个流程

java">    public static void main(String[] args) {ThreadLocal<User> threadLocal = new ThreadLocal<>();// 创建线程1Thread thread1 = new Thread(()->{User user = new User("user1");// 添加当前线程的变量,和其他线程隔离threadLocal.set(user);});// 设置名称、启动thread1.setName("thread1");thread1.start();}

ThreadLocal.set()方法的源码:

java">    public void set(T value) {// 得到当前线程对象Thread t = Thread.currentThread();// 得到当前线程对象中的MapThreadLocalMap map = getMap(t);// Map不为空就把值添加进去,this就是ThreadLocal对象,如果为空就创建一个Mapif (map != null) {map.set(this, value);} else {createMap(t, value);}}

ThreadLocal.get()也是类似的道理,先拿到当前线程对象,再拿到当前线程对象中的ThreadLocalMap,再从中取值。

最终还是应了开头那句话,可以把ThreadLocal看着一个工具类,可以用他来往当前线程中存储和获取值。

3、设置线程变量的2种方式
 

1)创建ThreadLocal对象时设置变量

创建ThreadLocal对象时设置初始化值,通过执行结果可以看出,每个线程在第一次调用get方法获取值的时候都会执行该段代码初始化变量,也就是每个线程得到的是一个新的对象,最终都存储到自己线程Thread的ThreadLocalMap容器中,不是同一个对象,也不是同一个存储容器,当然是隔离的。
java">@Data
class User {private String userName;public User() {}public User(String userName) {System.out.println("init user...");this.userName = userName;}
}public class Test {public static void main(String[] args) throws InterruptedException {// 创建ThreadLocal,并设置初始化值ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> {// 每个线程在不执行set方法设置变量的情况下,第一次调用get方法获取值的时候执行该段代码,初始化变量,也就是每个线程得到的是一个新的对象User user = new User("user1");return user;});// 创建线程1Thread thread1 = new Thread(()->{System.out.println("thread1 .......");threadLocal.get().setUserName("T1-user");System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());// 用完后清除,避免内存泄漏threadLocal.remove();});// 设置名称、启动thread1.setName("thread1");thread1.start();Thread.sleep(1000);// 创建线程2Thread thread2 = new Thread(()->{System.out.println("thread2 .......");System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());// 用完后清除,避免内存泄漏threadLocal.remove();});thread2.setName("thread2");thread2.start();}
}


2)通过set()方法设置变量

创建threadLocal对象,不设置初始化值,在各自的线程中通过set方法设置变量。

java">@Data
class User {private String userName;public User() {}public User(String userName) {System.out.println("init user...");this.userName = userName;}
}public class Test {public static void main(String[] args) throws InterruptedException {// 创建threadLocal对象,不设置初始化值ThreadLocal<User> threadLocal = new ThreadLocal<>();// 创建线程1Thread thread1 = new Thread(()->{User user = new User("user1");// 添加当前线程的变量,和其他线程隔离threadLocal.set(user);System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());// 用完后清除,避免内存泄漏threadLocal.remove();});// 创建线程2Thread thread2 = new Thread(()->{User user = new User("user2");// 添加当前线程的变量,和其他线程隔离threadLocal.set(user);System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());// 用完后清除,避免内存泄漏threadLocal.remove();});// 设置名称、启动thread1.setName("thread1");thread1.start();Thread.sleep(1000);thread2.setName("thread2");thread2.start();}
}

执行结果:

ThreadLocal%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98">4、关于ThreadLocal的内存泄漏问题

提到ThreadLocal,肯定都会想到内存泄漏,当ThreadLocalMap的Entry中的key为null,而value不为null时,该value就永远不能被访问到,就是一个无用的对象,按理来说应该被回收,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,如果当前线程没有结束,当前线程持有ThreadLocalMap,ThreadLocalMap持有Entry对象,Entry对象包含value,value可达从而不会被回收掉,这样就存在了内存泄漏。

1)话接上面,为什么ThreadLocalMap的Entry中的key会为null呢?

因为Entry中的key是弱引用,在垃圾回收的时候,如果key没有被其他对象引用,也就是说后续代码中不会再被用到,他就会被回收,最终Entry中的key为null。原来ThreadLocal对象在这里被引用,现在key为空,ThreadLocal在这里就没有被引用,如果其他地方也没有引用ThreadLocal对象,ThreadLocal对象就可以被回收,释放内存。

在使用完ThreadLocal后调用其remove方法,就可以清除不被使用的变量,避免内存泄漏。

java">        Thread thread2 = new Thread(()->{User user = new User("user2");// 添加当前线程的变量,和其他线程隔离threadLocal.set(user);System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());// 用完后清除,避免内存泄漏threadLocal.remove();});

ThreadLocal中,调用get、set、remove方法都会清除key为空的value,避免内存泄漏。

通过源码,可以追踪到 expungeStaleEntry 方法,该方法会清空key为空的value。



2)既然key是弱引用,GC回收会影响ThreadLocal的正常工作吗?

不会,因为有ThreadLocal变量引用着它,也就是说后面还会用到他,是不会被GC回收的,执行一段代码一探究竟。
 

java">        Thread thread1 = new Thread(()->{// 设置变量threadLocal.set(new User("thread1-user"));// 输出变量System.out.println(threadLocal.get().getUserName());System.gc(); //垃圾回收System.out.println("gc...gc");// 输出变量System.out.println(threadLocal.get().getUserName());});

执行结果:

可以看到,如果后续还会用到,是不会被回收的,不然问题就大了:“上一秒刚设置的变量,下一秒获取的时候就没了?”。



5、使用过程中的注意事项和误区

1)ThreadLocal与线程池

一般web容器,如tomcat就使用了线程池,或者我们自定义的线程池,线程池中的线程是存在复用情况的。如果我们在当前线程中使用ThreadLocal设置了一个变量,】并且没有执行remove方法,当前线程执行结束后,线程还在线程池中存在,线程并没有被销毁,下一个请求过来就会使用线程池中的线程,就会拿到上一个请求在线程中设置的变量。所以使用玩后一定要调用ThreadLocal的remove方法。

2)错误的理解导致使用方法
很多人看见这句话:“用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程”,就会理解为变量或者内存的完全隔离,就会出现错误的用法,例如:

错误方式一

java">@Data
class User {private String userName;public User() {}public User(String userName) {System.out.println("init user...");this.userName = userName;}
}public class Test {// 这是一个公共的变量public static User user = new User("user1");public static void main(String[] args) throws InterruptedException {ThreadLocal<User> threadLocal = new ThreadLocal<>();// 创建线程1Thread thread1 = new Thread(()->{// 设置变量threadLocal.set(user);// 线程1改变了user对象的值threadLocal.get().setUserName("thread1-user");System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());});// 创建线程2Thread thread2 = new Thread(()->{// 设置变量threadLocal.set(user);// 可拿到线程1中改变的user对象的值System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());});// 设置名称、启动thread1.setName("thread1");thread1.start();Thread.sleep(1000);thread2.setName("thread2");thread2.start();}
}

错误方式二

java">@Data
class User {private String userName;public User() {}public User(String userName) {System.out.println("init user...");this.userName = userName;}
}public class Test {// 这是一个公共的变量public static User user = new User("user1");public static void main(String[] args) throws InterruptedException {ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);// 创建线程1Thread thread1 = new Thread(()->{// 线程1改变了user对象的值threadLocal.get().setUserName("thread1-user");System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());});// 创建线程2Thread thread2 = new Thread(()->{// 可拿到线程1中改变的user对象的值System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());});// 设置名称、启动thread1.setName("thread1");thread1.start();Thread.sleep(1000);thread2.setName("thread2");thread2.start();}
}

执行结果:

是不是意外,不是说变量在线程之间是隔离的吗?怎么线程1改了user对象的值,线程二中也随之改变了呢?

因为ThreadLocal设置变量(对象)的时候,并不是拷贝一份新的变量(对象),而是直接赋值对象的引用,如果这个变量(对象)是一个公共变量(对象),那么各线程的ThreadLocalMap中的key所指向的其实还是同一个对象,并没有隔离。

代码说明:

java">public class Test {// 这是一个公共的变量public static User user = new User("user1");// ***** 线程不隔离// 不能到达user对象在各线程中互相隔离的效果, 因为user本身就是公共变量public static ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);// ***** 线程隔离public static ThreadLocal<User> threadLocal2 = ThreadLocal.withInitial(()-> {// 每个线程在不执行set方法设置变量的情况下,第一次执行get方法的时候都会执行本段代码,创建新的对象,现场之间使用的就不是同一个user对象User user = new User("user1");return user;});
}


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

相关文章

sqlite3 多线程和锁 ,优化插入速度及性能优化

一、 是否支持多线程&#xff1f; SQLite官网上的“Is SQLite threadsafe?”这个问答。 简单来说&#xff0c;从3.3.1版本开始&#xff0c;它就是线程安全的了。而iOS的SQLite版本没有低于这个版本的&#xff0c;当然&#xff0c;你也可以自己编译最新版本。 不过这个线程安全…

黑神话悟空无法登录服务器怎么办

黑神话悟空游戏在登录的时候会遇到无法登录服务器的问题&#xff0c;玩家可以采用一些有效的方法进行解决&#xff0c;其中最主要的措施就是优化网络环境和减少网络干扰。Rak小编为您整理黑神话悟空无法登录服务器如何解决的步骤及注意事项。 优化网络环境 1、当游戏无法登录服…

使用 PowerShell 自动化 Windows 系统管理任务

随着信息技术的迅速发展&#xff0c;系统管理任务的复杂性和重复性显著提高。作为 Windows 系统中的强大工具&#xff0c;PowerShell 不仅提供了命令行方式进行系统管理&#xff0c;还支持脚本编写来实现自动化&#xff0c;从而有效提高工作效率并减少人为错误。本文将深入探讨…

linux之ELK

ELK概述 ELK是一套开源的日志分析系统&#xff0c;由elasticsearchlogstashKibana组成。 官网说明:https://www.elastic.co/cn/products 首先: 先一句话简单了解E,L,K这三个软件 elasticsearch: 分布式搜索引擎 logstash: 日志收集与过滤&#xff0c;输出给elasticsearch Kiban…

linux安装go 环境

嗯&#xff0c;每个人的工作方法不一样&#xff0c;不喜勿喷哈 这是我安装的go 不是最新的 [rootsimetra-ecs-01 go]# go version go version go1.19.8 linux/amd64 [rootsimetra-ecs-01 go]# 首先先去下载golang的安装包&#xff0c;我用的是go1.19.8.linux-amd64.tar.gz 解…

如何使用ssm实现基于面向对象的学生事务处理系统分析与设计

TOC ssm138基于面向对象的学生事务处理系统分析与设计jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化…

工厂模式和策略模式区

工厂模式&#xff08;Factory Pattern&#xff09;和策略模式&#xff08;Strategy Pattern&#xff09;是两种常见的设计模式&#xff0c;它们都用于解决不同场景下的代码组织问题。以下是它们的区别和应用场景的详细比较&#xff1a; 工厂模式&#xff08;Factory Pattern&a…

MyBatis配置允许批量插入或更新数据

MyBatis配置allowMultiQueriestrue允许使用foreach标签批量插入或更新数据 执行update更新操作&#xff1a; <!-- 批量更新 --><update id"updateBatchByKey" parameterType"java.util.List"><foreach collection"list" item&q…