1. 从一次项目经历说起
在上家公司做spark的任务调度系统时,碰到过这么一个需求:
1.任务由一个线程执行,同时在执行过程中会创建多个线程执行子任务,子线程在执行子任务时又会创建子线程执行子任务的子任务。整个任务结构就像一棵高度为3的树。
2.每个任务在执行过程中会生成一个任务ID,我需要把这个任务ID传给子线程执行的子任务,子任务同时也会生成自己的任务ID,并把自己的任务ID向自己的子任务传递。
流程可由下图所示
解决方案有很多,比如借助外部存储如数据库,或者自己在内存中维护一个存储ID的数据结构。考虑到系统健壮性和可维护性,最后采用了jdk中的InheritableThreadLocal
来实现这个需求。
来看下InheritableThreadLocal的结构
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
InheritableThreadLocal继承自ThreadLocal,ThreadLocal可以说是一个存储线程私有变量的容器(当然这个说法严格来说不准确,后面我们就知道为什么),而InheritableThreadLocal正如Inheritable所暗示的那样,它是可继承的:使用它可使子线程继承父线程的所有线程私有变量。因此我写了个工具类,底层使用InheritableThreadLocal来存储任务的ID,并且使该ID能够被子线程继承。
public class InheritableThreadLocalUtils { private static final ThreadLocal<Integer> local = new InheritableThreadLocal<>(); public static void set(Integer t) { local.set(t); } public static Integer get() { return local.get(); } public static void remove() { local.remove(); } }
可以通过这个工具类的set方法和get方法分别实现任务ID的存取。然而在Code Review的時候,有同事觉得我这代码写的有问题:原因大概是InheritableThreadLocal在这里只有一个,子线程的任务ID在存储的时候会相互覆盖掉。真的会这样吗?为此我们用代码测试下:
public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<10;i++){ executorService.execute(new TaskThread(i)); } }static class TaskThread implements Runnable{ Integer taskId; public TaskThread(Integer taskId) { this.taskId = taskId; } @Override public void run() { InheritableThreadLocalUtils.set(taskId); ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(new Runnable() { @Override public void run() { System.out.println(InheritableThreadLocalUtils.get()); } }); } }
这段代码开启了10个线程标号从0到9,我们在每个线程中将对应的标号存储到InheritableThreadLocal,然后开启一个子线程,在子线程中获取InheritableThreadLocal中的变量。最后的结果如下
每个线程都准确的获取到了父线程对应的ID,可见并没有覆盖的问题。InheritableThreadLocal确实是用来存储和获取线程私有变量的,但是真实的变量并不是存储在这个InheritableThreadLocal对象中,它只是为我们存取线程私有变量提供了入口而已。因为InheritableThreadLocal只是在ThreadLocal的基础上提供了继承功能,为了弄清这个问题我们研究下ThreadLocal的源码。
2. ThreadLocal源码解析
ThreadLocal主要方法有两个,一个set用来存储线程私有变量,一个get用来获取线程私有变量。
2.1 set方法源码解析
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
Thread t = Thread.currentThread()获取了当前线程实例t,继续跟进第二行的getMap方法,
/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
t是线程实例,而threadLocals明显是t的一个成员变量,进入一探究竟
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是个什么结构?
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
ThreadLocalMap是类Thread中的一个静态内部类,看起来像一个HashMap,但和HashMap又有些不一样(关于它们的区别后面会讲),那我们就把它当一个特殊的HashMap好了。因此set方法中第二行代码
ThreadLocalMap map = getMap(t)是通过线程实例t得到一个ThreadLocalMap。接下来的代码
if (map != null) map.set(this, value); else createMap(t, value);
/** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
如果这个threadlocalmap为null,先创建一个threadlocalmap,然后以当前threadlocal对象为key,以要存储的变量为值存储到threadlocalmap中。
2.2 get方法源码解析
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */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(); }
首先获取当前线程实例t,然后通过getMap(t)方法得到threadlocalmap(ThreadLocalMap是Thread的成员变量)。若这个map不为null,则以threadlocal为key获取线程私有变量,否则执行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; }
protected T initialValue() { return null; }
首先获取threadlocal的初始化值,默认为null,可以通过重写自定义该值;如果threadlocalmap为null,先创建一个;以当前threadlocal对象为key,以初始化值为value存入map中,最后返回这个初始化值。
2.3 ThreadLocal源码总结
总的来说,ThreadLocal的源码并不复杂,但是逻辑很绕。现总结如下:
1.ThreadLocal对象为每个线程存取私有的本地变量提供了入口,变量实际存储在线程实例的内部一个叫ThreadLocalMap的数据结构中。
2.ThreadLocalMap是一个类HashMap的数据结构,Key为ThreadLoca对象(其实是一个弱引用),Value为要存储的变量值。
3.使用ThreadLocal进行存取,其实就是以ThreadLocal对象为隐含的key对各个线程私有的Map进行存取。
可以用下图的内存图像帮助理解和记忆
3. ThreadLocalMap详解
先看源码
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
3.1 ThreadLocalMap的key为弱引用
ThreadLocalMap的key并不是ThreadLocal,而是WeakReference
3.2 为何要用弱引用
减少了内存泄漏。试想我曾今存储了一个ThreadLocal对象到ThreadLocalMap中,但后来我不需要这个对象了,只有ThreadLocalMap中的key还引用了该对象。如果这是个强引用的话,该对象将一直无法回收。因为我已经失去了其他所有该对象的外部引用,这个ThreadLocal对象将一直存在,而我却无法访问也无法回收它,导致内存泄漏。又因为ThreadLocalMap的生命周期和线程实例的生命周期一致,只要该线程一直不退出,比如线程池中的线程,那么这种内存泄漏问题将会不断积累,直到导致系统奔溃。而如果是弱引用的话,当ThreadLocal失去了所有外部强引用的话,下次垃圾收集该ThreadLocal对象将被回收,对应的ThreadLocalMap中的key将为null。下次get和set方法被执行时将会对key为null的Entry进行清理。有效的减少了内存泄漏的可能和影响。
3.3 如何真正避免内存泄漏
及时调用ThreadLocal的remove方法
及时销毁线程实例
4. 总结
ThreadLocal为我们存取线程私有变量提供了入口,变量实际存储在线程实例的map结构中;使用它可以让每个线程拥有一份共享变量的拷贝,以非同步的方式解决多线程对资源的争用