如果你发现你的“此时此地”变得无法忍受并且使你非常不开心,这时你有三种选择:从这种状况中离开,改变它,或者完全接受它。
通过阅读这篇文章,你将可以学习到:
ThreadLocal
的使用方法与典型应用场景ThreadLocal
的核心概念——ThreadLocalMap
ThreadLocal
的设计思路——映射关系维护、生命周期管理ThreadLocal
使用过程中的那些大坑
接下来进入这些知识的分析和讲解过程。
ThreadLocal的使用方法与典型应用场景
ThreadLocal
是java.lang
包里面提供的类,正如其名字所示,它可以提供线程独占的变量,每一个线程,都可以拥有一份只有它自己使用的ThreadLocal
对象集合。不同线程之间,即使是同名的对象,也不是同一个实例。
该对象,如果不是手动释放,它的生命周期会始终持续到线程运行结束。
使用方法
java
代码解读
复制代码
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
:
这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】免费获取
java
代码解读
复制代码
new SimpleDateFormat("yyyyMMdd HHmm").format(date)
但SimpleDateFormat
对象并非线程安全的,这里截取它代码中的片段,不安全处已经用注释标明:
java
代码解读
复制代码
// 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
,即降低了重复创建对象造成的内存消耗,又能避免并发调用导致的状态问题。
java
代码解读
复制代码
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
的静态内部类,由于是静态,不持有外部对象引用,以下是源码,次要代码已略去。
java
代码解读
复制代码
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
。
java
代码解读
复制代码
// 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
。
java
代码解读
复制代码
// 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%的问题。