# ThreadLocal介绍
从官方文档来看:ThreadLocal类用来提供线程内部的局部变量。 这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
总结:
- 线程并发:在多线程并发的场景下
- 传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量
- 线程隔离:每个线程的变量都是独立的,不会相互影响
# 1.1 常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
# 1.2 ThreadLocal与synchronized 的区别
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用'以时间换空间'的方式, 只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用'以空间换时间'的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
# 2. ThreadLocal的内部结构
在JDK8中ThreadLocal每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。TheadLocal结构示意图
从上述可以总结出每个ThreadLocal是和Thread相关联的,每个Thread线程内部有一个ThreadLocalMap的结构。这个Map结构中key是当前Thread对应的ThreadLocal,value是一个Object。因此Thread和ThreadLocal是一对多的关系。。
# 2.1 设计好处
- 首先这样设计之后每个Map存储的Entry数量就会变少。Map存储大小是由ThreadLocal的数量决定的。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用
# 3. ThreadLocal核心源码分析
# 3.1 set方法
执行流程:
首先获取当前线程,并根据当前线程获取对应的ThreadLocalMap,如果Map不为null,则将参数设置到Map中(当前ThreadLocal的引用作为Key)如果Map为空,则给该线程创建对应的ThreadLocalMap
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
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);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
# 3.2 get方法
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
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;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
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;
}
# 3.3 remove方法
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
# 3.4 initialValue方法
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
#
从上面的源码上我们可以看出,无论是get、set、remove方法,首先基本都是先获取当前执行的线程,然后获取线程对应的threadLocalMap,之后再进行相应的操作。
# 4. ThreadLocalMap源码分析
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口。用其他方式实现了Map的功能。同时ThreadLocalMap里面还有一个内部类Entry用来实现类似于Map功能,保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从源码中可以看出Entry继承了WeakReference类,实现了弱引用,被弱引用关联的对象只能收集到下次垃圾收集为止,当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
ThreadLocalMap构造方法
/**
* 初始容量 —— 必须是2的整次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂。
*/
private Entry[] table;
/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化Entry数组,默认容量为16,但同时必须是2的次幂
table = new Entry[INITIAL_CAPACITY];
// 计算当前threadLocal的hashCode作为数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
从源码中简单可以分析出就是创建了一个Entry数组,计算当前threadLocal的hashCode值作为数组下标,key就是threadLocal,value就是对应的值。同时这里面包含一些数组下标计算,扩容阀值等都和HashMap中类似,有兴趣的同学可以演技一下
# 5. ThreadLocal内存泄漏问题
# 5.1 内存泄漏相关概念
- Memory overflow:内存溢出,没有足够的内存提供申请者使用。
- Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
# 5.2 内存泄漏分析
通过上面对内存泄漏概念的分析以及上面的总结,我们可以断定出Thread Local内存泄漏问题一般发生在多线程准确点来说是线程池中。因为如果是单线程的话,就例如平时我们写demo测试单线程的时候,一般程序执行结束单线程也就结束了,单线程就被回收了,通过上面的描述我们知道ThreadLocal是和对应的thread也就是线程绑定的,那么当线程都被回收了,线程对应的threadLocal肯定也被回收掉了。所以一般单线程场景下一般不会发生内存泄漏的问题。
而多线程场景下,如下图,假如我们有一个线程池例如Executors.newFixedThreadPool(4);,此时有多个任务进来,线程池根据设置创建4对应的线程处理对应的任务,而每个任务执行过程中都需要创建ThreadLocal来保存相应的值。任务执行结束,但是这个时候执行任务的对应的4个线程并不会结束,而是会继续处理正在排队的任务,任务继续执行,继续创建该线程的ThreadLocal。导致出现内存泄漏的问题。因此ThreadLocal内存泄漏主要是因为当线程执行完以后由于该线程未被回收,如果我们不手动执行remove方法,导致线程执行任务时创建的threadlocal没有被释放而导致的内存泄漏问题。
综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
因此要避免内存泄漏有两种方式:
- 使用完ThreadLocal,调用其remove方法删除对应的Entry
- 使用完ThreadLocal,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
# 5.3 为什么ThreadLocal里面的Entry要继承WeakReference
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。