提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
一、初步认识ThreadLocal
官方解释如下:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).
顾名思义:这个类提供线程局部变量,即每个线程访问一个(通过其* {@code get}或{@code set}方法)有自己的独立初始化变量的副本.
了解TeadLocal的概念是不够的,我们应该站在设计者的角度去探寻他的构造,才能更清晰的去去认识ThreadLocal.
- ThreadLocalMap
//从其实现来看,是类似HashMap容器的一种基于Key,value的容器
//特殊的是,它继承了弱引用,这也是引发内存泄漏的原因所在
//区别强引用,软引用,弱引用,虚引用的关键在于,他们面对gc时的态度,(弱引用,在gc发现弱引用后,会立即回收,生命周期更短)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//声明一个初始长度为16的table
table = new Entry[INITIAL_CAPACITY];
//threadLocalMap的hashCode与(16-1)取模,得到存放位置的容器下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
- Thread
public
class Thread implements Runnable {
//隶属于该线程的thradLcoal
ThreadLocal.ThreadLocalMap threadLocals = null;
}
- ThreadLocal类
总而言之:每一个Thread内部都维护了ThreadLocal.ThreadLocalMap这么一个对象,ThreadLocal则使用TreadLocalMap来存放当前线程的变量副本.
二、ThreadLocal使用
既然对ThreadLocal的概念有了初步的了解,那我们来看看他重要的API和实现.
日常开发中,ThreadLocal的使用其实很简单:
ThreadLocal<T> threadLocal = new ThreadLocal<>();
threadLocal.set();
threadLocal.get();
//remove方法很重要
threadLocal.remove();
再看下其方法实现:
//get方法
public T get() {
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap容器
ThreadLocalMap map = getMap(t);
if (map != null) {
//ThreadLocAL
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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);
}
set方法
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);
}
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();
//key值相同,覆盖原value
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();
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
/就是reference指向null,也就是将该entry的key指向null。方便后面对该entry进行清理
//Entry extends WeakReference extends Reference
e.clear();
// 清理staleSlot位置上的entry,继而staleSlot之后的key为null的entry都清理了,顺带将一些有哈希冲突的entry给填充回可用的index中。
expungeStaleEntry(i);
return;
}
}
}
#思考引发的安全问题
那么问题来了,ThreadLocal的使用非常简单,但是它作为并发工具类中一员,那么必然会存在并发安全的风险.
-
内存泄漏
ThreadLocal自身并不储存值,而是作为一个key来让线程从ThreadLocal获取value.Entry是中的key是弱引用,所以当gc时弱引用对象必然被回收,ThreadLocal必然会被回收.但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了ThreadLocal对象)使用了又不再调用get/set方法,就可能引发内存泄漏.这就需要我们在使用完ThreadLocal后,需要显式调用remove方法去删除.
-
池化技术的线程复用导致数据错乱
在之前我们也看到,ThreadLocal是真对线程自身的变量副本,那么问题来了,如果线程重复使用,那是不是有并发安全的风险呢?
线程复用,顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值.
此时我们假设这么一种场景,我们把tomcat线程池参数设置为1,在使用ThreadLocal,连续发送两个HTTP请求,此时我们从ThreadLocal中获取的值,肯定是相等的.所以,我们在使用时,应该在finally中显示的清除ThreadLocal中数据.