本篇文章主要介绍了ThreadLocal基本使用、实现原理以及从原理层面分析使用上的注意事项(入坑和脱坑指南),让大家能够在使用ThreadLocal的时候减少犯错可能。
什么是ThreadLocal
ThreadLocal是一个用于为每个线程提供独立变量副本的工具类。注意ThreadLocal仅仅是个工具类,而真正存储独立变量副本是Thread类。ThreadLocal只是提供了存储独立变量副本的数据结构ThreadLocalMap和操作当前线程独立变量副本的方法(get/set/remove)。
ThreadLocal的基本使用
//模拟动态切换数据源public class DynamicDataSourceEntity { public final static String DEFAULT_SOURCE = "DB_001"; private static final ThreadLocal local = new ThreadLocal<>(); private DynamicDataSourceEntity(){} public static String get(){ //获取线程独立变量副本 return local.get(); } //DB_2019 public static void set(String source){ //设置线程独立变量副本 local.set(source); } public static void set(int year){ local.set("DB_"+year); } public static void restore(){ local.set(DEFAULT_SOURCE); } public static void remove(){ //移除线程独立变量副本 local.remove(); } public static void main(String[] args) { for (int i=0; i<10; i++){ new Thread(()->{ LocalDate localDate = LocalDate.now(); DynamicDataSourceEntity.set(localDate.getYear()); System.out.println("select data"); DynamicDataSourceEntity.remove(); }).start(); } }}
上述代码模拟了动态切换数据源的场景,ThreadLocal的使用方式还是很简单的,核心方法如下:
- set:用于设置线程独立变量副本。没有set操作的ThreadLocal,容易引起脏数据。
- get:用于获取线程独立变量副本。没有get操作的ThreadLocal对象没有意义。
- remove:用于移除线程独立变量副本。没有remove操作,容易引起内存泄漏。
如果我们想为每个线程都先初始化一个默认值,可以用如下方式实现:
private static final ThreadLocal local = new ThreadLocal(){ @Override protected String initialValue() { //设置默认的数据源 return DEFAULT_SOURCE; }};
如上述代码所示,通过重写ThrealLocal的initialValue的方法来设置初始值,但是这个初始化过程并非在local对象加载静态变量的时候执行,而是在每个线程执行ThreadLocal.get()方法的时候执行。源代码如下:
public T get() { /** *每个线程都有自己的threadLocals变量用于存储独立变量副本(ThreadLocalMap类型), * threadLocals是一个map集合,key即ThreadLocal,value即独立变量副本 */ Thread t = Thread.currentThread(); //通过当前线程获取当前线程的threadLocals变量 ThreadLocalMap map = getMap(t); if (map != null) { //通过ThreadLocal获取Entry,从而拿到独立变量副本即e.value并返回 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //map == null 或 e==null时执行 return setInitialValue();}private T setInitialValue() { //初始化默认值 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value;}
每个线程都有自己的 ThreadLocalMap,如源码所示,map==null 或 e==null都会执行setInitialValue()。setInitialValue()中则执行了initialValue()方法并返回默认初始化的值,从而进行初始化操作。
深入剖析ThreadLocal
首先,我们通过ThreadLocal和Thread的类关系图来宏观了解下ThreadLocal和Thread。类关系图如下:
如图所示,ThreadLocalMap是ThreadLocal的静态内部类,Entry是ThreadLocalMap的静态内部类。Thread中的threadLocals变量类型为ThreadLocalMap,是用于存储独立变量副本的。ThreadLocalMap和ThreadLocal有三组对应的方法:get()、set()和remove(),在ThreadLocal中对它们只做了校验和判断,最终的实现会落在ThreadLocalMap上。Entry继承了WeakReference(弱引用),内部只有一个value成员变量,key是ThreadLocal对象。
Thread、ThreadLocal以及ThreadLocalMap三者的堆栈角度分析图如下:
从堆栈分析图可以看出,Thread中只有一个类型为ThreadLocalMap的threadLocals变量,ThreadLocalMap是一个Map结构的数据类型,key存储的是ThreadLocal对象(弱引用),value存储的是本地线程独占对象,同一个ThreadLocal对象可以被多个线程共享。
前面我们提了两次弱引用,到底什么是弱引用呢?
Java有四种引用类型,引用强度从强到弱依次为:强引用、软引用、弱引用和虚引用。这里我们只简单介绍弱引用,其他引用类型自行了解。
弱引用即WeakReference,表示如果弱引用的指向的对象只存在弱引用这一条线路,则下次YGC时会被回收。
先来看下Entry的源码:
static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; }}
Entry继承了WeakReference,key为弱引用,即只要ThreadLocal对象引用被设置为null,Entry的key就会自动在下一次YGC时被回收。同时ThreadLocal使用的set()和get()方法在调用的时候会把key==null的value置为null,使value能够被垃圾回收,避免内存泄漏。但是ThreadLocal是被多个线程共享的,通常作为私有静态变量使用,那么其生命周期不会随着线程结束而结束,所以这个弱引用的设计很鸡肋,没有起到应有的作用,反而增加了开发人员的理解难度。
InheritableThreadLocal详解
ThreadLocal用于同一个线程内,跨类、跨方法传递数据,但是很多情况下需要在线程中创建子线程,如果还是通过ThreadLocal传递数据子线程是获取不到的,这时候InheritableThreadLocal就派上用场了。InheritableThreadLocal继承自ThreadLocal,并且重写了getMap()、createMap()和childValue()方法。源码如下:
public class InheritableThreadLocal extends ThreadLocal { protected T childValue(T parentValue) { return parentValue; } //ThreadLocal获取的是Thread.threalLocals //而InheritableThreadLocal获取的是Thread.inheritableThreadLocals ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } //ThreadLocal创建的是Thread.threalLocals //而InheritableThreadLocal创建的是Thread.inheritableThreadLocals void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }}
Thread中有两个ThreadLocalMap类型的变量,一个是threalLocals,一个是inheritableThreadLocals。
- threalLocals:用于存储当前线程的独立变量。子类线程获取不到父类线程的数据。
- inheritableThreadLocals:用来解决父子线程独立变量共享问题。
通过Thread构造方法来了解InheritableThreadLocal是如何实现父子线程数据共享的,源码如下:
//JDK1.8源码 Thread构造函数最后都是通过调用init(标记2)的 构造的public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0);}//1private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true);}//2private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //省去一些无关紧要的代码 setPriority(priority); //inheritThreadLocals设置为true并且父类线程inheritableThreadLocals有共享数据则 //创建一个父类线程的inheritableThreadLocals副本,然后赋值给本线程的inheritableThreadLocals变量 //来实现父子线程数据共享 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID();}
ThreadLocal入坑与脱坑指南
ThreadLocal让线程可以安全地共享某个变量,但是很多开发者在使用ThreadLocal上存在一些问题,从而导致脏数据和内存泄漏的产生。下面看下ThrealLocal的入坑和脱坑指南:
入坑:
- 脏数据问题: 线程复用导致产生脏数据。由于线程池会复用Thread对象,进而Thread对象中的threalLocals也会被复用,导致Thread对象在执行其他任务时通过get()方法获取到之前任务设置的数据,从而产生脏数据。
- 内存泄漏问题: ThreadLocal通常是使用static关键字修饰的。如果开发人员单纯寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的Value,那么就会导致内存泄漏,Entry的Value无法被回收。
脱坑:
- 解决脏数据:线程执行前重新调用set()设置值。线程复用导致产生脏数据,如果复用线程在执行下个任务之前调用set()重新设置值,那么脏数据问题就不会出现了。
- 解决内存泄漏:线程执行完后调用remove()完成收尾工作。无法依托弱引用机制来回收Entry的Value,那就显式调用ThreadLocal的remove方法显式清除。
最后,Entry的弱引用机制不是导致ThreadLocal内存泄漏的原因,它的存在只是增加了开发人员的理解难度,就算没有弱引用机制,线程执行完不调用remove()清除也会存在内存泄漏问题。
END
笔者是一位热爱互联网、热爱互联网技术、热于分享的年轻人,如果您跟我一样,我愿意成为您的朋友,分享每一个有价值的知识给您。喜欢作者的同学,点赞+转发+关注哦!
点赞+转发+关注,私信作者“读书笔记”即可获得BAT大厂面试资料、高级架构师VIP视频课程等高质量技术资料。