一、ThreadLocal简介
1、概述
1、JDK1.2提供,位于java.lang包,ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量或线程本地存储
。
2、实际上,就ThreadLocal这个类来讲,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。
3、作用:
-
用于实现线程内的数据共享
,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离(避免了线程安全问题),就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
-
方便同一个线程复杂逻辑下的数据传递
,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。
2、常用方法
方法 | 说明 |
---|
public T get() | 返回当前线程的此线程局部变量副本中的值 |
protected T initialValue() | 返回此线程局部变量的当前线程的“初始值” |
public void remove() | 删除此线程局部变量的当前线程值 |
public void set(T value) | 将此线程局部变量的当前线程副本设置为指定值 |
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) | 创建一个线程局部变量。变量的初始值是通过调用Supplier上的get方法来确定的。 |
public class ThreadLocalTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("张三", "李四", "王五", "赵六", "陈七");
User user = new User();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
int bonus = new Random().nextInt(5000) + 4000;
user.addSalary(bonus);
System.out.println(Thread.currentThread().getName() + " 的工资为:" + user.threadLocal.get());
} finally {
user.threadLocal.remove();
}
}, list.get(i)).start();
}
}
}
class User {
int salary;
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);
public void addSalary(int bonus) {
Integer basicSalary = threadLocal.get();
salary = basicSalary + bonus;
threadLocal.set(salary);
}
}
3、注意事项
1、在阿里Java开发手册中提到:必须回收自定义的ThreadLocal变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的ThradLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代码中使用try-finally块进行回收,在finally中调用remove()方法
。代码演示及运行结果如下:
2、解决方法:使用try-finally块进行回收,在finally中调用remove()方法,正确结果如下:
public class ThreadLocalTest {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
User user = new User();
try {
for (int i = 0; i < 5; i++) {
threadPool.submit(() -> {
try {
int bonus = new Random().nextInt(5000) + 4000;
System.out.println(Thread.currentThread().getName() + " 初始值为:" + user.threadLocal.get());
user.addSalary(bonus);
System.out.println(Thread.currentThread().getName() + " 计算后的值为:" + user.threadLocal.get());
} finally {
user.threadLocal.remove();
}
}, threadPool);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
class User {
int salary;
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);
public void addSalary(int bonus) {
Integer basicSalary = threadLocal.get();
salary = basicSalary + bonus;
threadLocal.set(salary);
}
}
二、ThreadLocal分析
1、Thread、ThreadLocal、ThreadLocalMap三者关系
1、每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,用于存放线程本地变量(key为ThreadLocal对象,value为要存储的数据),这样,线程之间的ThreadLocalMap互不干扰。threadLocals变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化
。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
2、ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。
3、ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。
public class ThreadLocal<T> {
static class ThreadLocalMap {
}
public ThreadLocal() {
}
}
4、当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值
。
5、三者关系图:
2、ThreadLocalMap源码
1、ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。
2、在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3
,扩容为原容量的2倍
,在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
3、ThreadLocal的set方法
1、set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:
-
获取当前线程的成员变量threadLocals
-
如果threadLocals不等于null,则调用set方法存放数据,方法结束
-
否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
如果threadLocals不等于null,则调用ThreadLocalMap中的set方法存放数据
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
4、ThreadLocal的get方法
1、对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,其他线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:
-
获取当前线程的成员变量threadLocals
-
如果threadLocals非空,调用getEntry方法尝试查找并返回节点e
-
如果e不为null,说明找到了,那么返回e的value,方法结束
-
如果e为null,说明没找到,方法继续。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
三、ThreadLocal的内存泄漏
1、概述
2、为什么使用弱引用包装的ThreadLocal对象作为key
1、如果某个Entry直接使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时已经无法访问这个对象,就造成了key的内存泄漏
。
2、因此ThreadLocal对象被包装为弱引用作为key
。当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了
。
3、引发问题:
-
由于Entry中的key(ThreadLocal对象)是弱引用,当外部的ThreadLocal对象的强引用被置为null时,那么系统GC时,根据可达性分析,这个ThreadLocal对象没有任何一条链路能够引用它,势必会被回收。
-
这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如使用线程池,线程池中的线程会被复用),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成value的内存泄漏。
-
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄漏。所以在不使用某个ThreadLocal对象后,要手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
4、从前面的set、getEntry、remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry
。
3、总结
1、ThreadLocal能实现线程的数据隔离,在于Thread的ThreadLocalMap,所以ThreadLocal可以只初始化一次,只分配一块内存空间即可,没必要作为成员变量多次被初始化,因此建议使用static修饰
。
2、ThreadLocalMap的key为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除
。
3、应用场景:
-
使用ThreadLocal的典型场景是数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
-
Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
-
Spring声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。