ThreadLocal设计思想浅析通过阅读这篇文章,你将可以学习到: ThreadLocal的使用方法与典型应用场景 T
如果你发现你的“此时此地”变得无法忍受并且使你非常不开心,这时你有三种选择:从这种状况中离开,改变它,或者完全接受它。

通过阅读这篇文章,你将可以学习到:
ThreadLocal
的使用方法与典型应用场景ThreadLocal
的核心概念——ThreadLocalMap
ThreadLocal
的设计思路——映射关系维护、生命周期管理ThreadLocal
使用过程中的那些大坑
接下来进入这些知识的分析和讲解过程。
ThreadLocal的使用方法与典型应用场景
ThreadLocal
是java.lang
包里面提供的类,正如其名字所示,它可以提供线程独占的变量,每一个线程,都可以拥有一份只有它自己使用的ThreadLocal
对象集合。不同线程之间,即使是同名的对象,也不是同一个实例。
该对象,如果不是手动释放,它的生命周期会始终持续到线程运行结束。
使用方法
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
一个典型的用例如上,使用泛型声明一个静态的ThreadLocal
变量,通过覆写initialValue
函数,为这个变量赋值,同时,AtomicInteger
保证了不同线程之间,生成变量值的唯一性。这样,线程内部通过调用ThreadId.get()
,可以获取到该线程唯一的id
。
可以思考下,如果不借助
ThreadLocal
,要如何实现上述“为每个Thread
赋值唯一的threadId
的需求——在Thread
类里面增加成员变量threadId
,并且在Thread
的构造函数里,由一个全局Factory
为该threadId
赋值(同样是借助AtomicInteger
)。不仅实现方案要繁琐许多,而且一旦这个变量不是Integer
类型,而是String
或者Object
呢?势必需要大量全局Factory
。
典型应用场景
ThreadLocal
的应用场景有3个最主要的特征:
- 并发环境,多个线程需要访问同一个对象
- 该对象在被调用时,内部状态会发生不可预测的变化
- 该对象创建成本高
典型的就是SimpleDateFormat
,我们知道它一般用于格式化日期字符串,将Date
对象转化为String
:
new SimpleDateFormat("yyyyMMdd HHmm").format(date)
但SimpleDateFormat
对象并非线程安全的,这里截取它代码中的片段,不安全处已经用注释标明:
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date); // 注意!这里calendar是成员变量,在并发调用情况下,旧值会被新值覆盖
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
在并发环境下,我们当然可以为每一个线程单独创建一个SimpleDateFormat
对象,用于格式化日期。但要知道,SimpleDateFormat
对象创建成本是很高的,这显然会对程序运行效率造成负面影响。另一个手段是,在访问该变量的函数上加锁,通过临界区来保证同一时间下只有一条线程使用该对象。方案二同样不利于程序运行效率,rejected!。
这时便可以使用ThreadLocal
,即降低了重复创建对象造成的内存消耗,又能避免并发调用导致的状态问题。
public class Foo
{
// SimpleDateFormat is not thread-safe, so give one to each thread
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue()
{
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public String formatIt(Date date)
{
return formatter.get().format(date);
}
}
ThreadLocal的核心概念
ThreadLocal
本质上是通过类似HashMap
的键值对结构,来维护Thread
独占的实例集合。
核心概念之ThreadLocalMap
ThreadLocalMap
是ThreadLocal
的静态内部类,由于是静态,不持有外部对象引用,以下是源码,次要代码已略去。
static class ThreadLocalMap {
// 继承自WeakReference,将传入的ThreadLocal对象包装为key,value则是对应泛型的值
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 下述capacity、table、size、threshold等,用于实现类似HashMap的数据存储和扩容功能
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
// 饱汉式,懒加载
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);
}
// 用于子线程拷贝父线程的ThreadLocal对象,createInheritedMap
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (Entry e : parentTable) {
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++;
}
}
}
}
// 存储KV
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)]) { // for循环找到首个可用entry
if (e.refersTo(key)) { // 若已有key,则更新value
e.value = value;
return;
}
if (e.refersTo(null)) { // 否则插入
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 容量检查
rehash();
}
// 使用完一定要remove,否则会导致Entry持有object引用(即使key变为了null,但value仍然指向Object)
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)]) {
// Android-changed: Use refersTo().
if (e.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
// 对外暴露的set方法,将ThreadLocal对象加入到当前Thread的ThreadLocalMap中
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
// 对外暴露的get方法,可见本质上每个线程自己创建并维护了一个Object
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();
}
}
ThreadLocalMap与Thread的关系
每一个Thread
对象内部,都有2个Map
,分别是自身的threadLocals
以及继承自父亲的inheritableThreadLocals
。
// Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Entry是WeakReference的子类
Entry
是一个维护了键值对的数据包装类,Key为ThreadLocal<Object>
对象,Value为该变量对应的Object
对象实例。为了避免对ThreadLocal
对象造成强引用,使Entry继承自WeakReference<ThreadLocal>
。
类HashMap实现ThreadLocal对象集合
ThreadLocalMap
内部实际上维护了一个Entry[]
数组,命名为table
,其初始大小16
,超过2/3
则触发扩容。在插入时,index
的计算方式为key.threadLocalHashCode & (len - 1)
,其中key
就是ThreadLocal
类型的对象。
ThreadLocal的设计思路——映射关系维护、生命周期管理
映射关系维护
根据上述分析,我们可以将Thread
及ThreadLocal
的关系用下图表示:
这里可以明确一个核心思想,即ThreadLocal
机制并不是把一个对象分散给多个Thread
使用,实际上它是对每个Thread
都创建一个该对象实例,并且交给Thread
自身进行维护。理解了这句话,也就理解了ThreadLocal
机制的本质。
为ThreadLocal
对象设值有两种方式,一是通过覆写initialValue()
函数,被动初始化;二是主动调用set()
函数进行初始化。
生命周期管理
已知如上的引用关系中,存在2条引用链(这里Object
对象代指ThreadLocal
通过泛型包装的对象):
- 从
Thread
对象到ThreadLocal
对象的虚引用:Thread->ThreadLocalMap->Entry->WeakRef<ThreadLocal>
- 从
Thread
对象到Object
对象的强引用:Thread->ThreadLocalMap->Entry->Object
建立这两种引用关系的时机很好理解,就是向ThreadLocalMap
里面添加对象时,也就是覆写initialValue()
或者调用set()
的时机。当ThreadLocal
对象使用完毕后,该Entry
并不会自动移除,而是仍然保留在Thread
的ThreadLocalMap
成员变量里(这不难理解,Thread
本身又不知道这个对象后续是否要使用)。
由于Entry
对于ThreadLocal
对象的引用为WeakRef
,当JVM发生GC时会释放该引用,从而使Entry
的Key
变为null
。在源代码里,将这种Key为null
的Entry
称为StaleEntry(过期条目)
。很明显,过期条目持有了Object
的强引用,如果不进行释放,会导致严重的内存泄漏。ThreadLocal
类里面提供了expungeStaleEntry(i)
函数用来清理StaleEntry
。
// ThreadLocal.java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
调用上述函数,就会遍历清理全部的StaleEntry
,以下场景会触发这个流程:
- 场景一:
Thread
运行结束,释放ThreadLocalMap
,其中的Entry
自然就被释放了 - 场景二:在调用
set(Object)
时,恰好命中了一个StaleEntry
- 场景三:主动对
ThreadLocal
对象调用remove()
函数
场景一不适用于线程池的场景,此时线程长期存活,无法释放。
场景二随机性太大,需要计算Key的hash
值时恰好命中上一个被释放的Entry
,不可控。
因此,最稳妥的方式是场景三。这也是在使用ThreadLocal
时容易出错的地方,没有主动调用release()
方法,导致有内存泄漏的风险。
ThreadLocal使用过程中的那些大坑
到这里其实也没啥好讲的,只要记住“用完要释放”,就能避免99%的问题。
转载自:https://juejin.cn/post/7410280735116083227