目录
1、set(ThreadLocal key, Object value) 源码解析:
六、编程规范推荐使用 static final 修饰 ThreadLocal 对象
一、ThreadLocal 的基本使用
ThreadLocal 是位于 JDK 的 java.lang 核心包中。
如果程序创建了一个
ThreadLocal
实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。
“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那一个本地值也会被释放。
在多线程并发操作“线程本地变量”时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。
1、ThreadLocal 的成员方法
方法 | 说明 |
---|---|
public void set(T var1) | 设置当前线程在“线程本地变量”实例中绑定的本地值 |
public T get() | 获得当前线程在“线程本地变量”实例中绑定的本地值 |
public void remove() | 移除当前线程在“线程本地变量”实例中绑定的本地值 |
2、小案例:
public class ThreadLocalDemo {
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + ":" + threadLocal.get());
//清除本地内存中的本地变量
threadLocal.remove();
}
public static void main(String[] args) {
Thread thread1 = new Thread(){
@Override
public void run() {
//设置线程1中本地变量的值
threadLocal.set("thread1");
//调用打印方法
print("thread1");
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
threadLocal.set("thread2");
print("thread2");
}
};
thread1.start();
thread2.start();
}
}
输出:
thread1:thread1
thread2:thread2
二、ThreadLocal 使用场景
1、线程隔离
ThreadLocal
的主要价值在于线程隔离,
ThreadLocal
中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。
2、跨函数传递数据
通常用于同一个线程内,跨类、跨方法传递数据时,如果不用
ThreadLocal
,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。
三、ThreadLocal 原理和源码分析
1、set(T value)方法
set(T value)
方法用于设置“线程本地变量”在当前线程的
ThreadLocalMap
中对应的值,相当于设置线程本地值,其核心源码如下:
public void set(T var1) {
//获取当前线程对象
Thread var2 = Thread.currentThread();
//获取当前线程的 ThreadLocalMap 实例属性
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//判断 map 是否存在
if (var3 != null) {
//value 被绑定到 threadLocal 实例
var3.set(this, var1);
} else {
this.createMap(var2, var1);
}
}
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals;
}
//创建一个 ThreadLocalMap 成员
//并为新的 Map 成员设置第一个 Key-Value 对,Key 为当前的 ThreadLocal 实例
void createMap(Thread var1, T var2) {
var1.threadLocals = new ThreadLocal.ThreadLocalMap(this, var2);
}
2、get( )方法
get( )
方法用于获取“线程本地变量”在当前线程的
ThreadLocalMap
中对应的值,相当于获取线程本地值,其核心源码如下:
public T get() {
// 获得当前线程对象
Thread var1 = Thread.currentThread();
// 获得线程对象的 ThreadLocalMap 内部成员
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
// 如果当前线程的内部 map 成员存在
if (var2 != null) {
// 以当前 threadlocal 为 Key,尝试获得条目
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
// 条目存在
if (var3 != null) {
Object var4 = var3.value;
return var4;
}
}
// 如果当前线程对应 map 不存在或者 map 存在,但是当前 threadlocal 实例没有对应的 Key-Value,返回初始值
return this.setInitialValue();
}
// 设置 threadlocal 关联的初始值并返回
private T setInitialValue() {
//调用初始化钩子函数,获取初始值
Object var1 = this.initialValue();
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if (var3 != null) {
var3.set(this, var1);
} else {
this.createMap(var2, var1);
}
return var1;
}
3、remove()方法
remove()
方法用于在当前线程的
ThreadLocalMap
中,移除“线程本地变量”所对应的值,其核心源码如下:
public void remove() {
ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
if (var1 != null) {
var1.remove(this);
}
}
4、initialValue( ) 方法
当“线程本地变量”在当前线程的
ThreadLocalMap
中尚未绑定值时,
initialValue( )
方法用于获取初始值。其源码如下:
protected T initialValue() {
return null;
}
JDK
已经为大家定义
了一个 ThreadLocal
的内部
SuppliedThreadLocal
静态子类,并且提供了
ThreadLocal.withInitial(…) 静态工厂方法,方便大家在定义 ThreadLocal
实例时设置初始值回调函数。使用工厂方法构造ThreadLocal 实例的代码如下:
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "初始值");
SuppliedThreadLocal的源码如下:
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> var0) {
return new ThreadLocal.SuppliedThreadLocal(var0);
}
//内部静态子类
//继承了 ThreadLocal,重写了 initialValue()方法,返回钩子函数的值作为初始值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
//保存钩子函数
private final Supplier<? extends T> supplier;
//传入钩子函数
SuppliedThreadLocal(Supplier<? extends T> var1) {
this.supplier = (Supplier)Objects.requireNonNull(var1);
}
protected T initialValue() {
return this.supplier.get();
}
}
四、ThreadLocalMap 源码分析
static class ThreadLocalMap {
// Map 的条目初始容量 16
private static final int INITIAL_CAPACITY = 16;
// Map 的条目数组,作为散列表使用
private ThreadLocal.ThreadLocalMap.Entry[] table;
// Map 的条目数量
private int size;
// 扩容因子
private int threshold;
...省略
// Map 的条目类,一个静态的内部类
// Entry 继承 WeakReference,Key 为 ThreadLocal 实例
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
}
ThreadLocal
源码中
get
()、
set
( )、
remove
()方法都涉及到
ThreadLocalMap
的方法调用,主要调用了 ThreadLocalMap
的如下几个函数:
- set(ThreadLocal<?> key, Object value) :向 Map 实例设置“Key-Value 对”。
- getEntry(ThreadLocal):从 Map 实例获取 Key(ThreadLocal 实例)所属的 Entry。
- remove(ThreadLocal):根据 Key(ThreadLocal 实例)从 Map 实例移除所属的 Entry。
1、set(ThreadLocal<?> key, Object value) 源码解析:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据 key 的 HashCode,找到 key 在数组上的槽点 i
int i = key.threadLocalHashCode & (len-1);
// 从槽点 i 开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
// 如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到现有槽点:Key 值为 ThreadLocal 实例
if (k == key) {
e.value = value;
return;
}
// 找到异常槽点:槽点被 GC 掉,重设 Key 值和 Value 值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 没有找到现有的槽点,增加新的 Entry
tab[i] = new Entry(key, value);
// 设置 ThreadLocal 数量
int sz = ++size;
// 没有可清理的 Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
2、Entry 的 Key 需要使用弱引用
Entry
用于保存
ThreadLocalMap
的“
Key-Value
”条目,但是
Entry
使用了对
Threadlocal
实例进行包装之后的弱引用(WeakReference
)作为
Key
,其代码如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
问题:
为什么 Entry 需要使用弱引用对 Key 进行包装,而不是直接使用 Threadlocal 实例作为 Key呢?
通过分析以下示例来找到答案
public void fun(){
// 创建一个线程本地变量
ThreadLocal threadLocal= new ThreadLocal<Integer>();
// 设置值
threadLocal.set(999);
// 获取值
threadLocal.get();
//末尾
}
当线程thread
执行 fun 方法到其末尾时,线程 thread相关的 JVM 栈内存以及内
ThreadLocalMap 成员的结构为:
线程 thread调用 fun()方法,新建了一个 ThreadLocal 实例,并使用 threadLocal 局部变量指向这个实例,并且此 threadLocal是强引用;在调用 threadLocal.set(999)
之后,线程 thread的 ThreadLocalMap
成员内部
会新建一个 Entry
实例,其
Key
以弱引用包装的方式指向
ThreadLocal
实例。
当线程 thread执行完 fun方法后,fun的方法栈帧将被销毁,强引用 threadLocal 的值也就没有了,
但此时线程的
ThreadLocalMap
里的对应的
Entry
的
Key
引用还指向了
ThreadLocal
实例。
若
Entry 的 Key
引用是强引用,就会导致
Key
引用指向的
ThreadLocal
实例、及其
Value
值都不能被
GC 回收,这将造成严重的内存泄露,如下图:
五、 什么是弱引用呢?
仅有弱引用(WeakReference)指向的对象,只能生存到下一次垃圾回收之前。
由于
ThreadLocalMap
中
Entry
的
Key
使用了弱引用,在下次
GC
发生时,就可以使那些没
有被其他强引用指向、仅被
Entry
的
Key
所指向的
ThreadLocal
实例能被顺利回收。并且,在
Entry 的 Key
引用被回收之后,其
Entry
的
Key
值变为
null
。后续当
ThreadLocal
的
get
、
set
或
remove
被调用时,ThreadLocalMap
的内部代码会清除这些
Key
为
null
的
Entry
,从而完成相应的内存释放。总结一下,使用 ThreadLocal
会发生内存泄漏的前提条件:
- 线程长时间运行而没有被销毁。线程池中的 Thread 实例很容易满足此条件。
- ThreadLocal 引用被设置为 null,且后续在同一 Thread 实例的执行期间,没有发生对其他 ThreadLocal 实例的 get、set 或 remove 操作。
六、编程规范推荐使用 static final 修饰 ThreadLocal 对象
ThreadLocal
实例作为
ThreadLocalMap
的
Key
,针对一个线程内所有操作是共享的,所以建议设置 static
修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用 static
修饰 ThreadLocal
就会节约内存空间。另外,为了确保
ThreadLocal
实例的唯一性,除了使用
static修饰之外,还会使用 final 进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:
final static ThreadLocal<String> TD = new ThreadLocal<>();
凡事都有两面性,使用
static
、
final
修饰
ThreadLocal
实例也会带来副作用:这使得
Thread 实例内部的 ThreadLocalMap
中
Entry
的
Key
在
Thread
实例的生命期内将始终保持为非
null
,从而导致 Key
所在的
Entry
不会被自动清空,这就会导致
Entry
中的
Value
指向的对象一直存在强引用,Value
指向的对象在线程生命期内不会被释放,最终导致内存泄露。所以,使用
static
、
final修饰 TheadLocal
实例,使用完后必须使用
remove
()进行手动释放。
如果使用线程池,可以定制线程池的
afterExecute 方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用 TheadLocal 实例的 remove()方法对其手动释放,从而实现的其线程内部的 Entry 得到释放,参考的代码如下:
private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
ExecutorService threadPoolExecutor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
//异步任务执行完成之后的钩子方法
@Override protected void afterExecute(Runnable target, Throwable t) {
//清空 TheadLocal 实例的本地值
START_TIME.remove();
}
};