项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下面就介绍下ThreadLocal是如何做到线程内本地变量传递的
1. ThreadLocal介绍
1.1 官方介绍
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结: 线程并发: 在多线程并发的场景下 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量 线程隔离: 每个线程的变量都是独立的,不会互相影响
1.2 基本使用
1.2.1 常用方法
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明 描述 ThreadLocal() 创建ThreadLocal对象 public void set( T value) 设置当前线程绑定的局部变量 public T get() 获取当前线程绑定的局部变量 public void remove() 移除当前线程绑定的局部变量
使用案例:
先来看下基本用法:
private static ThreadLocal tl = new ThreadLocal<>(); public static void main(String[] args) throws Exception { tl.set(1); System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(ThreadLocalTest::fc).start(); } private static void fc() { System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s", Thread.currentThread().getName(), tl.get())); }
运行结果:
当前线程名称: main, main方法内获取线程内数据为: 1 当前线程名称: main, fc方法内获取线程内数据为: 1 当前线程名称: Thread-0, fc方法内获取线程内数据为: null
可以看到,main线程内任意地方都可以通过ThreadLocal获取到当前线程内被设置进去的值,而被异步出去的fc调用,却由于替换了执行线程,而拿不到任何数据值,那么我们现在再来改造下上述代码,在异步发生之前,给Thread-0线程也设置一个上下文数据:
private static ThreadLocal tl = new ThreadLocal<>(); public static void main(String[] args) throws Exception { tl.set(1); System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s", Thread.currentThread().getName(), tl.get())); fc(); new Thread(()->{ tl.set(2); //在子线程里设置上下文内容为2 fc(); }).start(); Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行 fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响 } private static void fc() { System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s", Thread.currentThread().getName(), tl.get())); }
运行结果为:
当前线程名称: main, main方法内获取线程内数据为: 1 当前线程名称: main, fc方法内获取线程内数据为: 1 当前线程名称: Thread-0, fc方法内获取线程内数据为: 2 当前线程名称: main, fc方法内获取线程内数据为: 1
可以看到,主线程和子线程都可以获取到自己的那份上下文里的内容,而且互不影响。
ThreadLocal与synchronized的区别
ThreadLocal方案的好处
- 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
- 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
ThreadLocal的内部结构
常见的误解
如果我们不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal 确实是这样设计的,但现在早已不是了。
现在的设计
但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
具体的过程是这样的:
- 每个Thread线程内部都有一个Map (ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
这样设计的好处
这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:
- 这样设计之后每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
ThreadLocal的核心方法源码
set方法
public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null) // 存在则调用map.set设置此实体entry map.set(this, value); else // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal t.threadLocals = new ThreadLocalMap(this, firstValue); }
执行流程:
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
get方法
public T get() { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 如果此map存在 if (map != null) { // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e ThreadLocalMap.Entry e = map.getEntry(this); // 对e进行判空 if (e != null) { @SuppressWarnings("unchecked") // 获取存储实体 e 对应的 value值 // 即为我们想要的当前线程对应此ThreadLocal的值 T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue() { // 调用initialValue获取初始化的值 // 此方法可以被子类重写, 如果不重写默认返回null T value = initialValue(); // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null) // 存在则调用map.set设置此实体entry map.set(this, value); else // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); // 返回设置的值value return value; }
执行流程
A. 首先获取当前线程, 根据当前线程获取一个Map
B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C. 如果e不为null,则返回e.value,否则转到D
D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
remove方法
public void remove() { // 获取当前线程对象中维护的ThreadLocalMap对象 ThreadLocalMap m = getMap(Thread.currentThread()); // 如果此map存在 if (m != null) // 存在则调用map.remove // 以当前ThreadLocal为key删除对应的实体entry m.remove(this); }
执行流程
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
initialValue方法
protected T initialValue() { return null; }
此方法的作用是 返回该线程局部变量的初始值。
(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
(2)这个方法缺省实现直接返回一个null。
(3)如果想要一个除null之外的初始值,可以重写此方法。
ThreadLocalMap源码分析
基本结构:
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
(1) 成员变量
private static final int INITIAL_CAPACITY = 16; private Entry[] table; private int size = 0; private int threshold; // Default to 0
跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。
(2) 存储结构 - Entry
static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
弱引用和内存泄漏
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
(1) 内存泄漏相关概念
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak:
内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
(2) 弱引用相关概念
Java中的引用有4种类型: 强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用:
强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果key使用强引用
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
如果key使用弱引用
那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
出现内存泄漏的真实原因
- 没有手动删除这个Entry
- CurrentThread依然运行
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
为什么使用弱引用
根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
- 使用完ThreadLocal,调用其remove方法删除对应的Entry
- 使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)