ThreadLocal 原理及源码分析
强引用:普通引用
软引用:一般用于缓存
弱引用:一般用于Map集合的清理,如ThreadLocalMap清理
虚引用:JVM中用于管理直接内存
一、ThreadLocal 是什么?
ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
场景:
(1)ThreadLocal 的经典使用场景是数据库连接和 session 管理等。
(2)Spring的Transaction的实现、MyBatis的分页实现等等。
二、测试类
因为ThreadLocal 为每个使用该变量的线程提供
独立的变量副本
,所以导致如下代码中,线程2在从ThreadLocal 中get()时获取不到线程1存入的值,导致抛出异常。
package com.company.test;
import com.company.entity.User;
/**
* @author zhangxh
* @Description:
* @date 2020-08-14
*/
public class ThreadLocalTest {
static ThreadLocal tl = new ThreadLocal();
public static void main(String[] args) {
new Thread(()->{
tl.set(new User(1, "张三", 23));
User user = (User) tl.get();
System.out.println(user.getName()); // 张三
},"线程1").start();
new Thread(()->{
User user = (User) tl.get();
System.out.println(user.getName());
//Exception in thread "线程2" java.lang.NullPointerException
},"线程2").start();
}
}
二、源码原理分析
2.1 ThreadLocal 的set方法原理
首先,我们使用
ThreadLocal tl = new ThreadLocal();
创建了一个ThreadLocal对象,然后在tl.set(new User(1, "张三", 23));
的时候,将tl
变量(即所指向对象的hash值)作为key、set方法的参数new User()
作为value设置到ThreadLocal中的ThreadLocalMap中(这里ThreadLocalMap为每个线程对象Thread的独有的全局变量ThreadLocals,这样就把对象与每个线程“绑定起来”,即作为线程的变量副本,线程间互不影响)。
2.2 ThreadLocal内存泄漏问题(为什么Entry要使用弱引用?)
假设 Entry使用强引用,如下图,当我们不想使用ThreadLocal时,此时将 tl
强引用置为null时,由于ThreadLocalMap中存在强引用指向ThreadLocal对象,故gc仍然无法回收ThreadLocal对象,造成对象无法回收问题(造成内存泄漏)。
若Entry使用弱引用(如下图),当我们不想使用ThreadLocal时,设置 tl=null
,此时由于key的引用指向ThreadLocal对象为弱引用(特点:只要经历gc即被回收),所以弱引用可以解决此处的内存泄漏
问题。
但当经历gc之后,ThreadLocal被回收,对应的key值变成null,此处会导致该key为null的Entry数据对应的value所占内存再也无法被访问到(即无法被回收),造成内存泄露
问题。
解决方法:
注:
(1)尽管ThreadLocal在set和get时,会自行清理key为null的Entry对象,但不能时刻保证key为null的Entry被及时清理,故
建议当我们不想使用ThreadLocal时,需要手动执行tl.remove();
以释放对应内存。
(2)如果threadLocals位于线程池中,每次使用完务必清理本地的threadLocals,防止其他线程从线程池中拿到该对象后,里面的旧数据影响其他线程。
2.3 ThreadLocalMap中的Hash算法与处理Hash一致性问题(线性探测)
ThreadLocalMap与HashMap相似,但解决冲突方法不一样,HashMap采用的是拉链法
,ThreadLocalMap采用线性探测法
。