文章目录
目录
介绍
ThreadLocal 是为解决多线程程序的并发问题提供了一种新的思路,ThreadLocal 为每个线程提供独立的数据副本,避免多个线程共享变量时的竞态条件。
在JDK1.2版本中就提供了java.lang.ThreadLocal;在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>,API方法也相应进行了调整。
原理
每个Thread对象都有一个ThreadLocalMap(称为 threadlocals),底层存储结构为 Entry[]
,当创建一个ThreadLocal的时候,就会将 ThreadLocal 对象添加到该Map中,其中键就是ThreadLocal,值是数据的副本对象。其中,key的引用为弱引用,这样设计的目的是防止内存泄漏。
底层代码
- 核心方法:
public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
// Thread类中的核心成员
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
// ThreadLocalMap内部类
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // Key是弱引用
value = v; // Value是强引用
}
}
private Entry[] table;
}
- set()方法工作流程:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) {
map.set(this, value); // this指当前ThreadLocal对象
} else {
createMap(t, value); // 首次使用创建Map
}
}
// 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) { // 清理过期Entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 新建Entry
}
- get()方法实现:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
Entry e = map.getEntry(this); // 以ThreadLocal为Key查找
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 初始化值(默认null)
}
存在的潜在问题
内存泄漏问题
- 原因:ThreadLocal 实例被回收,但Entry 中的 Value 仍然被强引用且未手动删除,同时线程并未销毁,例如使用线程池导致线程未销毁。
- 解决方法:不使用时手动remove。
数据污染问题
- 原因:使用线程池导致线程未销毁,某个线程在任务1设置ThreadLocal 值后,又被该线程执行任务2获取值,可能读取到任务1设置的值。
- 解决方法:线程池中慎用。
使用场景
-
线程上下文传递(如Spring的
RequestContextHolder
) -
数据库连接管理(
Connection
per Thread) -
SimpleDateFormat线程安全封装
-
用户会话信息存储