一、概述
ThreadLocal(线程局部变量)是Java中的一个类,它提供了一种可以将数据与线程绑定的机制。每个ThreadLocal对象可以绑定一个特定的值,这个值只能被对应的线程访问和修改,其他线程无法访问。这种机制可以保证各个线程中的数据是相互独立的,避免了多线程并发访问时的线程安全问题。
ThreadLocal通常被用于多线程环境下需要共享数据,但又要保证数据安全的情况。比如,在web应用中每个请求都会有一个对应的线程,而这个请求可能需要访问某些共享数据,但又不能让其他请求的线程访问,这时候就可以使用ThreadLocal将数据与请求线程绑定起来,实现数据的隔离和安全。
二、实现原理
每个线程对象内部都维护着一个ThreadLocalMap(类似于HashMap),ThreadLocalMap内部数据结构是一个由Entry对象组成的数组(默认长度为16)。所谓的Entry对象其实就是一个键值对,key为ThreadLocal(弱引用,方便被回收),value为存储的数据。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始化Entry数组
//根据threadLocal的Hash编码与数组长度-1做与运算算出下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将新创建的Entry对象存入数组对应下标位置
table[i] = new Entry(firstKey, firstValue);
//长度为1
size = 1;
//容量为16
setThreshold(INITIAL_CAPACITY);
}
当我们在某一个线程内部使用ThreadLocal存储值时,其实就是在该线程对象内部的ThreadLocalMap中存入了一个以ThreadLocal为key,value为值的键值对。
三、常用方法与源码阅读
3.1 set()
存储数据至当前线程的 ThreadLocalMap : public void set(T value)
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程的全部信息
ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap
if (map != null)
map.set(this, value);//有这个map时以当前threadLocal为键,value为值存入map
else
createMap(t, value);//没有则使用creatMap()方法创建Map
}
void createMap(Thread t, T firstValue) {
//以当前threadLocal为键,value为值初始化创建一个ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.2 get()
从当前线程的 ThreadLocalMap 中获取数据: public T get()
public T get() {
Thread t = Thread.currentThread();//获取当前线程的全部信息
ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap
if (map != null) {
//若map不为空,根据threadLocal获取到对应的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//若Entry对象不为空,获取到对应的value并返回
T result = (T)e.value;
return result;
}
}
//若为空执行setInitialValue()方法
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//不为空存入键值对
map.set(this, value);
else
//为空创建新Map
createMap(t, value);、
//返回初始化值
return value;
}
3.3 remove()
从当前线程的 ThreadLocalMap 中删除数据: public void remove()
一定要在 finally 代码块中,调用 remove() 方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。 remove() 方法中会把 Entry 中的 key 和 value 都设置成 null ,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程内部的Map集合
if (m != null)
//有map则根据threadLocal删除该Entry对象
m.remove(this);
}
3.4 下标计算
根据threadLocalHashCode计算下标时如出现哈希冲突,则使用开放定址法寻找新的下标。
private static int nextIndex(int i, int len) {
//如果计算下标不是数组最后一位则返回该位置的下一位,是则从头开始寻找
return ((i + 1 < len) ? i + 1 : 0);
}
四、为什么用ThreadLocal做key?
如果在应用中,一个线程中只使用了一个 ThreadLocal 对象,那么使用 Thread 做 key 也是可以的,代表每个 Thread 线程对应一个 value 。但是,在实际应用程序开发过程中,一个线程中很有可能不只使用了一个 ThreadLoca l 对象。这时使用 Thread 做 key 就会产生混淆。
五、父子线程共享数据
在这种情况下使用 ThreadLocal 是行不通的。 main 方法是在主线程中执行的,相当于父线程。在 main 方法中开启了另外一个线程,相当于子线程。两个线程对象,各自拥有不同的 ThreadLocalMap 。应该使用 InheritableThreadLocal ,它是 JDK 自带的类,继承了 ThreadLocal 类。从他的源码可以发现共享数据的原理是创建子线程的“一刹那”将父线程的ThreadLocalMap复制给子线程。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
六、ThreadLocal应用场景
6.1 线程数据隔离
ThreadLocal 的主要价值在于线程隔离,ThreadLocal 中的数据只属于当前线程,该数据对别的线程是不可见的,起到隔离作用。这样操作,可以在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。
6.2 跨函数传递
数据通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal ,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。