ThreadLocal 简单介绍
一、什么是ThreadLocal?
从名字我们就可以看到 ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。
二、ThreadLocal如何使用?
ThreadLocal的变量通常用private static修饰:
package com.linhuaming.test;
import java.util.HashMap;
import java.util.Map;
/**
* 本地线程 测试
*/
public class ThreadLocalTest {
private static ThreadLocal<Map<String,Object>> LOCAL = new ThreadLocal<>();
//打印出本线程内的localVar值
public static void printLoca(){
Map<String, Object> map = LOCAL.get();
System.out.println(map);
//清除本地内存中的本地变量
ThreadLocalTest.LOCAL.remove();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Map<String,Object> map = new HashMap<>();
map.put("username","lhm");
map.put("password","123456");
map.put("age",28);
ThreadLocalTest.LOCAL.set(map);
printLoca();
}, "t1");
t1.start();
Thread.sleep(2000);
Thread t2 = new Thread(() -> {
Map<String,Object> map = new HashMap<>();
map.put("username","wja");
map.put("password","123456");
map.put("age",29);
ThreadLocalTest.LOCAL.set(map);
printLoca();
}, "t2");
t2.start();
}
}
注意:
- ThreadLocal的泛型是Object的,可以定义成map、set、list等等
- 一个类中可以定义多个ThreadLocal属性
运行结果:
三、ThreadLocal的实现原理是什么?
1、set()方法
实现过程:
- 首先获取当前线程对象,并获取当前线程的ThreadLocalMap。
- 如果这个map为null,就调用createMap()方法初始化map,初始化需要传入泛型和要存储的值。
- 如果map不为null,就调用set的重载方法,它会将当前线程对象作为键,存储值。
如下图所示:
2、ThreadLocalMap
createMap()方法
其实本地线程对象的threadLocals属性,就是用来存储ThreadLocalMap的。
ThreadLocalMap是ThreadLocal的静态内部类。它用Entry保存数据,而且继承了弱引用,
Entry内部使用ThreadLocal类型的变量作为键,保存传入的值。
3、get()方法
实现过程:
- 获取当前线程对象。
- 如果map不为null,就通过ThreadLocal对象,取出对应的Entry。
- 如果entry不为空,就获取Entry中的Value,返回。
- 如果前一步中map为空,就调用setInitialValue()方法。
如下图所示:
setInitialValue()方法
这个方法是给ThreadLocal设置初始值,如下图所示:
4、remove()方法
将ThreadLocal的值,从当前线程的ThreadLocalMap中删除,如下图所示:
5、总结
如果读懂了上述 get() 和 set() 方法,会发现 Map 存储在线程之上,在 ThreadLocal 中通过Thread.currentThread()来得到此线程,然后通过这个线程来获取这个线程对应的 Map ,而Map上的数据存储方式采用的是 <key,value> ,其中 key 的值是 ThreadLocal 自己,而value的值是我们想要获取的值,如下图所示:
四、ThreadLocal 数据共享
既然 ThreadLocal 设计的初衷是解决线程间信息隔离的,那 ThreadLocal 能不能实现线程间信息共享呢?
答案是肯定的,只需要使用 ThreadLocal 的子类 InheritableThreadLocal 就可以轻松实现,来看具体实现代码:
public static void main(String[] args) {
ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
inheritableThreadLocal.set("测试");
new Thread(() -> System.out.println(inheritableThreadLocal.get())).start();
}
五、ThreadLocal在Java中的应用场景有哪些?
1、Spring实现事务隔离级别
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
2、解决日期的线程安全
项目中有部分用户的时间出错,发现是多个线程共享一个SimpleDataFormat的问题。
使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add()。
如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
但是每个线程内部都new一个SimpleDataFormat对象也不太好,所以使用ThreadLocal包装SimpleDataFormat,解决了线程安全的问题。
3、多个方法调用
一个线程经常需要横跨多个方法调用,那么它的参数就必须层层传递,给每个方法都加上相同的参数不太优雅。
而且,如果中间遇到第三方类库,参数就无法传递了。可以使用ThreadLocal,开始时把参数存进去,需要时直接get取出即可。
4、JDBC的数据库连接
从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。
数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题。
六、常见问题
1、Entry的key为什么设计成弱引用?
前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference
对象,即被设计成了弱引用。
那么,为什么要这样设计呢?
假如key对ThreadLocal对象的弱引用,改为强引用。
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
此时,如果执行该代码的线程
使用了线程池
,一直长期存在,不会被销毁。
就会存在这样的强引用链
:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC
回收,于是产生了内存泄露
问题。
为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用
。
弱引用
的对象,在GC做垃圾清理的时候,就会被自动回收了。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
如下图所示:
接下来,最关键的地方来了。
由于当前的ThreadLocal变量已经被指向null
了,但如果直接调用它的get
、set
或remove
方法,很显然会出现空指针异常
。因为它的生命已经结束了,再调用它的方法也没啥意义。
此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get
、set
或remove
,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
这样就能最大程度的解决内存泄露
问题。
需要特别注意的地方是:
- key为null的条件是,ThreadLocal变量指向
null
,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用? - 如果当前ThreadLocal变量指向
null
了,并且key也为null了,但如果没有其他ThreadLocal变量触发get
、set
或remove
方法,也会造成内存泄露。
下面看看弱引用的例子:
public static void main(String[] args) {
WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
System.out.println(weakReference0.get());
System.gc();
System.out.println(weakReference0.get());
}
打印结果:
java.lang.Object@1ef7fe8e
null
传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。
但如果出现下面这种情况:
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
}
执行结果:
java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e
先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。
我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。
如果将object强引用设置为null:
public static void main(String[] args) {
Object object = new Object();
WeakReference<Object> weakReference1 = new WeakReference<>(object);
System.out.println(weakReference1.get());
System.gc();
System.out.println(weakReference1.get());
object=null;
System.gc();
System.out.println(weakReference1.get());
}
执行结果:
java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null
第二次gc之后,弱引用能够被正常回收。
由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。
此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?
答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。
而相比之下,Entry的key,管理的地方就非常明确了。
这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。
2、ThreadLocal真的会导致内存泄露?
通过上面的Entry对象中的key设置成弱引用,并且使用get
、set
或remove
方法清理key为null的value值,就能彻底解决内存泄露问题?答案是否定的。
如下图所示:
假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get
、set
或remove
方法。
那么,Entry的value值一直都没被清空。
所以会存在这样一条强引用链
:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。
其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露
。
3、如何解决内存泄露问题?
前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?
答:有办法,调用ThreadLocal对象的remove
方法。
不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:
先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。
public class CurrentUser {
private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
public static void set(UserInfo userInfo) {
THREA_LOCAL.set(userInfo);
}
public static UserInfo get() {
THREA_LOCAL.get();
}
public static void remove() {
THREA_LOCAL.remove();
}
}
然后在业务代码中调用相关方法:
public void doSamething(UserDto userDto) {
UserInfo userInfo = convert(userDto);
try{
CurrentUser.set(userInfo);
...
//业务代码
UserInfo userInfo = CurrentUser.get();
...
} finally {
CurrentUser.remove();
}
}
需要我们特别注意的地方是:一定要在finally
代码块中,调用remove
方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。
remove
方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
参考:
https://blog.csdn.net/m0_62609939/article/details/129493213
https://blog.csdn.net/weixin_45560850/article/details/124868500