ThreadLocal 原理和内存泄漏
概念
ThreadLocal 通常被我们称为线程本地变量
作用
在 Java 的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到 ThreadLocal 类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象在 Java 的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到 ThreadLocal 类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象
ThreadLocal 如何做到为每个线程存有一份独立的本地值呢?
ThreadLocal 内部的维护着一个 Map
当工作线程 Thread 实例向本地变量保持某个值时,会以“ Key - Value 对”(即键﹣值对)的形式保存在 ThreadLocal 内部的 Map 中,其中 Key 为线程 Thread 实例, Value 为待保存的值。当工作线程 Thread 实例从 ThreadLocal 本地变量取值时,会以 Thread 实例为 Key ,获取其绑定的 Value 。
验证
public class ThreadTest {
public static void main(String[] args) {
ThreadLocal<Object> objectThreadLocal = new ThreadLocal<>();
Dog dog = new Dog();
objectThreadLocal.set(dog); //被下面的覆盖,因为key 一样
objectThreadLocal.set("sada");
System.out.println(objectThreadLocal.get());
}
}
ThreadLocal 的使用场景
(1)线程隔离
ThreadLocal 的主要价值在于线程隔离,可以为每个线程绑定一个用户会话信息、数据库连接、 HTTP 请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源。比如:可以为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常,
(2)跨函数传递数据
就是在不同方法取值,因为都是操作本地当前线程,都可以获取数据,避免通过参数传递数据带来的耦合
项目中使用到本地线程存储用户信息
public class UserThreadLocal {
private UserThreadLocal(){}
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<SysUser>();
public static void put(SysUser sysUser){
LOCAL.set(sysUser);
}
public static SysUser get(){
return LOCAL.get();
}
public static void remove(){
LOCAL.remove();
}
}
版本的变化
在早期的 JDK 版本中, ThreadLocal 的内部结构是一个 Map ,Map拥有者为 ThreadLocal
在 JDK 8版本中的变化,Map拥有者为 Thread (线程)实例,每一个 Thread 实例拥有一个 Map ( ThreadLocalMap ).实例。Map 结构的 Key 值也发生了变化:新的 Key 为 Thread -Local 实例。
如果给一个 Thread 创建多个 ThreadLocal 实例,然后放置本地数据,那么当前线程的 ThreadLocalMap 中就会有多个“ Key - Value 对”其中 ThreadLocal 实例为 Key ,本地数据为 Val ue 。.
源码
ThreadLocal源码提供的方法不多,主要有:set(T value)方法、get()方法、remove()方法和initialValue()方法。
set
ThreadLocal.ThreadLocalMap
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
// 获取 当前线程的ThreadLocalMap成员
ThreadLocalMap map = getMap(t);
//判断map 是否存在
if (map != null)
// 将当前的value 放到map 中 key 为当前的ThreadLocal
map.set(this, value);
else
//创建 一个 ThreadLocalMap
createMap(t, value);
}
//获取线程t 对应的ThreadLocalMap 成员
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//和上面 map.set(this, value); 一样
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get
public T get() {
//获取当前线程 //获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap成员
ThreadLocalMap map = getMap(t);
//判断不为空
if (map != null) {
//当前的ThreadLocal 为key ,尝试获取数据
ThreadLocalMap.Entry e = map.getEntry(this);
//数据存在
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
//返回
return result;
}
}
//返回初始值
return setInitialValue();
}
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//移除当前ThreadLocal
m.remove(this);
}
内存泄漏问题
概念
不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃甚至系统崩溃。
其实在上面源码的时候,大家有看到Entry ,这是ThreadLocalMap的内部类。
而且 Entry 继承了 WeakReference ,并使用 WeakReference 对 Key 进行
包装
static class Entry extends WeakReference<ThreadLocal<?>> {
为什么 Entry 需要使用弱引用对 Key 进行包装,而不是直接使用 ThreadLocal 实例作为 Key 呢?
举例子
当线程 tn 执行完 funcA (方法后,funcA0)的方法栈帧将被销毁,强引用 local 的值也就没有了,但此此时线程的 ThreadLocalMap 中对应的 Entry 的 Key 引用还指向 ThreadLocal 实例。如果 Entry 的 Key 引用是强引用,就会导致 Key 引用指向的 ThreadLocal 实例及其 Value 值都不能被 GC 回收,这将造成严重的内存泄漏问题。
前提了解:弱引用无论内存如何,都会回收的
内存泄漏value
value为线程变量的副本。ThreadLocal生命周期始终伴随着当前线程的,但是当前线程是什么时候回收,是不确定的。
强引用。使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
解释:如果ThreadLocal 进行了一次垃圾回收,此时key 没有了,value 还有。
所以每次用完后调用remove 方法