介绍
ThreadLocal是一个线程变量工具类,提供了线程局部变量,就是为每一个使用该变量的线程都提供一个变量值的副本。我们可以利用ThreadLocal创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对ThreadLocal变量的引用,这两个线程也无法看到彼此的ThreadLocal变量。
常用方法
1 . public T get() 获取当前线程的副本变量值。
// 获取当前线程中的变量副本
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程中的ThreadLocalMap对象
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;
}
}
// 若没有该变量副本,返回setInitialValue()
return setInitialValue();
}
2 . public void set(T value) 保存当前线程的副本变量值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// map不为空,直接将ThreadLocal对象作为key
// 变量本身的值为value,存入map
map.set(this, value);
else
// 否则,创建ThreadLocalMap
createMap(t, value);
}
3 . public void remove() 移除当前前程的副本变量值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
4 . public static ThreadLocal withInitial(Supplier supplier) 初始化变量
//使用示例
public class ThreadLocalDemo {
public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
System.out.println("invoke initial value");
return "default value";
});
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
THREAD_LOCAL.set("first thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("second thread");
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
THREAD_LOCAL.set("third thread");
THREAD_LOCAL.remove();
System.out.println(THREAD_LOCAL.get());
}).start();
new Thread(() ->{
System.out.println(THREAD_LOCAL.get());
}).start();
SECONDS.sleep(1L);
}
}
// 输出:
first thread
second thread
invoke initial value
default value
invoke initial value
default value
实现原理
ThreadLocalMap是ThreadLocal的核心,定义在ThreadLocal类里的内部类,他维护了一个Enrty数组。ThreadLocal存/取数据都是通过操作Enrty数组来实现的。
Enrty数组作为一个哈希表,将对象通过开放地址方法散列到这个数组中。作为对比,HashMap则是通过链表法将对象散列到数组中。
开放地址法就是元素散列到数组中的位置如果有冲突,再以某种规则在数组中找到下一个可以散列的位置,而在ThreadLocalMap中则是使用线性探测的方式向后依次查找可以散列的位置。
[✎ 这个挺好,也是一种哈希碰撞的解决思路]
ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。
✔ 每个Thread中都有一个ThreadLocal.ThreadLocalMap对象。
public class ThreadLocal<T> {
// ThreadLocalMap是ThreadLocal的内部类
static class ThreadLocalMap {
// Entry类,内部key对应的是ThreadLocal的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
// 变量的副本,强引用
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
扩容
哈希表一般都有扩容操作,那么它是如何触发扩容和如何扩容的呢? 在ThreadLocalMap中有一个阈值threshold=table长度 * 2/3。当size>=threshold时,遍历table并删除key为null的元素,如果删除后size>=threshold*3/4时,需要进行扩容操作。
// 扩容阈值(threshold = 底层哈希表table的长度 len * 2 / 3)
private void rehash() { expungeStaleEntries(); if (size >= threshold - threshold / 4) resize(); }
扩容操作比较简单,但是会先判断key是否为null,如果为null,将对应的value也设置为null,帮助gc。扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,在扩容的时候,会把key为null的Entry的value值设置为null. 以便内存回收,减少内存泄漏问题。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
应用场景
1 线程资源一致性
我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。
当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。
这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低「编程难度」。
2 分布式计算
3 比较常见的例子,应该是SimpleDateFormat了,这个对象在多线程下会出现一定的问题,一般在高并发的场景下,都会使用ThreadLocal给每个线程分配一个SimpleDateFormat对象。
private static ThreadLocal<SimpleDateFormat> sdf = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
String msg = null;
try {
// 调用get方法 -> withInitial初始化
msg = sdf.get().format(date);
} finally {
// 用完 remove
sdf.remove();
}
return msg;
}
4 全局变量
某些数据比如用户ID,很可能在整条业务线上多个方法中都需要用到,如果通过方法参数的形式一层一层的传递下去,整体代码显得凌乱不优雅,这时可以通过ThreadLocal的方式存储。通常可以通过AOP或者拦截器的方式进行赋值,执行完业务逻辑之后调用remove()方法。
private final static ThreadLocal<UserInfo> TL_USER = new ThreadLocal<>();
TL_USER.set(userInfo);
UserInfo userInfo = TL_USER.get();
TL_USER.remove();
5 mybatis
Mybatis使用SqlSessionManager保证了我们同一个线程取出来的连接总是同一个。它是如何做到的呢?其实很简单,就是内部使用了一个ThreadLocal。
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
// 创建连接
public void startManagedSession() {
this.localSqlSession.set(openSession());
}
// 取连接
@Override
public Connection getConnection() {
final SqlSession sqlSession = localSqlSession.get();
if (sqlSession == null) {
throw new SqlSessionException("Error: Cannot get connection. No managed session is started.");
}
return sqlSession.getConnection();
}
常见问题
原因
ThreadLocal threadlocal1 = new ThreadLocal();
强引用:threadlocal1对应图中【1】
弱引用:Key(WeakReference(threadlocal1))对应图中的【2】
但是当我们把threadlocal1 =null;也就是1处,断开强引用时,此时ThreadLocal对象只有一个弱引用,那么GC发生时,ThreadLocal对象被回收了,Entry变成了一个key为null的Entry。也叫脏Entry
[☛ 重点:ThreadLocalMap中的key是ThreadLocal对象,这个是一个弱引用,在gc时会被回收,此时这个节点就变成了一个没有key的节点,不会被回收,成为了脏数据,所以要显示的调用remove方法确保这种情况不发生]
特点是:
key为null,value不能被应用程序访问到,因为我们已经没有引用到他的引用了
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value链存在,当前线程迟迟不结束(例如线程池),但不能被使用,成了脏数据,造成了内存泄漏。
过程如图:
内存泄漏问题
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。