继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

ThreadLocal原理深入解析

料青山看我应如是
关注TA
已关注
手记 332
粉丝 97
获赞 353

1. 从一次项目经历说起

在上家公司做spark的任务调度系统时,碰到过这么一个需求:
1.任务由一个线程执行,同时在执行过程中会创建多个线程执行子任务,子线程在执行子任务时又会创建子线程执行子任务的子任务。整个任务结构就像一棵高度为3的树。
2.每个任务在执行过程中会生成一个任务ID,我需要把这个任务ID传给子线程执行的子任务,子任务同时也会生成自己的任务ID,并把自己的任务ID向自己的子任务传递。
流程可由下图所示
https://img2.mukewang.com/5b51a4890001bd9207890443.jpg

解决方案有很多,比如借助外部存储如数据库,或者自己在内存中维护一个存储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中的变量。最后的结果如下
https://img3.mukewang.com/5b51a4940001b27408510213.jpg

每个线程都准确的获取到了父线程对应的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进行存取。

可以用下图的内存图像帮助理解和记忆
https://img3.mukewang.com/5b51a4da0001844909270674.jpg

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对象的只有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结构中;使用它可以让每个线程拥有一份共享变量的拷贝,以非同步的方式解决多线程对资源的争用

原文出处


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP