ThreadLocal
ThreadLocal 本地线程变量,主要用于解决数据访问的竞争,通常用于多租户、全链路压测、链路跟踪中保存 线程上下文环境,在一个请求流转中非常方便的获取一些关键信息,例如当前的租户信息、压测标记。
ThreadLocal 正如其名,本地线程变量,即数据存储在线程自己的局部变量中。
其整体架构如下图所示:
ThreadLocal的核心设计理念总结如下:
每一个线程对象会维护一个私有属性:ThreadLocal.ThreadLocalMap threadLocals
ThreadLocalMap 内部结构为Key-Value键值对,其Key为ThreadLocal对象,Value为调用ThreadLocal的set方法设置的值。
一言以蔽之:ThreadLocal是将线程需要访问的数据存储在线程对象自身中,从而避免多线程的竞争。
Thread类
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal 及 ThreadLocal.ThreadLocalMap
public class ThreadLocal<T> {
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
static class ThreadLocalMap {
/**
* 自定制的hash map entry,不同于hashmap的Node<K,V>,这里的节点key是ThreadLocal类型,而value可以指定类型
* 思考:为什么使用弱引用类型key?【重要】
*/
static class Entry extends WeakReference<ThreadLocal<?>> { //弱引用
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//ThreadLocal.ThreadLocalMap threadLocals
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
}
简单使用:
public class TLDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
//放进threadlocalmap
UserContext.set(new UserInfo(finalI + "", null, null));
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
//从threadlocalmap中取
UserInfo info1 = UserContext.get();
System.out.println(Thread.currentThread().getName() + " " + info1);
}, i + "").start();
}
}
}
class UserContext {
private static final ThreadLocal<UserInfo> userContext = new ThreadLocal<>();
public static UserInfo get() {
return userContext.get();
}
public static void set(UserInfo ui) {
userContext.set(ui);
}
}
ThreadLocal为何被设计为弱引用?
Map 中的用于存储键值对的 Entry 为什么要继承 WeakReference?
思考这个问题之前先和大家普及一下 Java 的 4 种引用类型,主要是在垃圾回收时 java 虚拟机会根据不同的引用类型采取不同的措施。
- 强引用:java默认的引用类型,例如 Object a = new Object();其中 a 为强引用,new Object() 为一个具体的对象。一个对象从根路径能找到强引用指向它,jvm 虚拟机就不会回收。
- 软引用(SoftReference):进行年轻代的垃圾回收不会触发 SoftReference 所指向对象的回收;但如果触发 Full GC,那 SoftReference 所指向的对象将被回收。备注:是除了软引用之外没有其他强引用引用的情况下。
- 弱引用(WeakReference) :如果对象除了有弱引用指向它后没有其他强引用关联它,当进行年轻代垃圾回收时,该引用指向的对象就会被垃圾回收器回收。
- 虚引用(PhantomeReference) 该引用指向的对象,无法对垃圾收集器收集对象时产生任何影响,但在执行垃圾回收后垃圾收集器会通过注册在 PhantomeReference 上的队列来通知应用程序对象被回收。
从四种弱引用的实际作用来说,主要是与垃圾回收器配合,决策什么时候可以将被引用的对象回收。
根据第一部分,声明了一个 TheadLocal 对象,并且一个线程通过调用 threadLocal 对象的set(Object value) 存储了一个对象,其引用如上图所示。
ThreadLocal 的设计比较晦涩难懂,究其原因是 我们通过 threadLocal 对象的 set 方法进行存储值,但数据并不是存储在 ThreadLocal 对象中,而是存储在当前调用该方法的线程对象中。但从应用者的角度来看,我们操作的对象是ThreadLocal,从设计上来说就应该为它考虑。
试问一个问题:如果应用程序觉得 ThreadLocal 对象的使命完成,将 threadLocal ref 设置为null,如果 Entry 中引用 ThreadLocald 对象的引用类型设置为强引用的话,会发生什么问题?
答案是:ThreadLocal 对象会无法被垃圾回收器回收,因为从 thread 对象出发,有强引用指向 threadlocal obj。此时会违背用户的初衷,造成所谓的内存泄露。
由于 ThreadLocalMap 中的 key 是指向 ThreadLocal,故从设计角度来看,设计为弱引用,将不会干扰用户释放 ThreadLocal 的意图。
另一个问题:关于大量 Entry 造成的内存泄漏问题探讨
延展问题①:ThreadLocal 无法在父子线程之间传递数据
使用 InheritableThreadLocal
延展问题②:InheritableThreadLocal 的局限性
InheritableThreadLocal 支持子线程访问在父线程中设置的线程上下文环境的实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时
但我们提到并发、多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱
使用 TransmittableThreadLocal