在上家公司做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<ThreadLocal>
,这是一个弱引用,说它弱是因为如果一个对象只被弱引用引用到,那么下次垃圾收集时就会被回收掉。如果引用ThreadLocal对象的只有ThreadLocalMap的key,那么下次垃圾收集过后该key就会变为null。
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结构中;使用它可以让每个线程拥有一份共享变量的拷贝,以非同步的方式解决多线程对资源的争用