×

深入理解ThreadLocal

96
zhangke3016
2017.10.15 11:57* 字数 1500
ThreadLocal

一、ThreadLocal是什么

先看JDK源码中对ThreadLocal的解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
这个类提供线程局部变量。这些变量与普通的变量不同,因为每个访问的线程(通过其get或set方法)都有自己的独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态(private static)字段(例如:一个用户ID或事务ID)。

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).
只要线程存活并且ThreadLocal实例可以访问,每个线程都保存对其线程局部变量副本的隐含引用; 线程结束之后,线程本地实例的所有副本都将被垃圾回收(除非存在对这些副本的其他引用)。

根据ThreadLocal的源码注释中,先说结论:

  1. ThreadLocal使各线程能够保持各自独立初始化的对象。也就是说,通过ThreadLocal.set()方法设置的变量为当前线程的独立对象,可通过ThreadLocal.get()获取与当前线程绑定的独立对象。
  2. 当线程结束之后,线程内所存放的变量对象都将被垃圾回收。也就是说,存入的变量对象是在Thread内存放的,这个后面详细介绍。
  3. 一个ThreadLocal在一个线程中只能对应存储一个对象,如果调用多次set()方法,则最后一个set的对象会覆盖掉之前存入的对象。如果需要存放其他的对象,就再创建一个新的ThreadLocal出来。
  4. 建议将ThreadLocal变量定义成private static的,原因下面说。
  5. ThreadLocal存储的变量对象在线程生命周期内有效,方便我们在当前线程内部,在不同方法不同对象内取出该变量,避免了将这个对象作为参数传递。当然我们也可以要把线程共享的对象通过ThreadLocal放到线程中,但没有必要。
  6. 我们存入的变量对象实际存放在Thread对象的ThreadLocal.ThreadLocalMap threadLocals变量中,在ThreadLocalMap内以ThreadLocal为key,以存入的变量对象为value。各线程通过自己的内部变量管理自身存储的对象。

二、ThreadLocal怎么用

ThreadLocal公共方法

ThreadLocal的使用方式相对还是很简单的,作为一个工具类,主要提供了get set remove三个对存储变量操作的方法。

 private static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();
    public static void main(String[] args) throws Exception {
        sThreadLocal.set(" main ");
        System.out.println(Thread.currentThread().getName() +" ---->"+sThreadLocal.get());

        for (int i = 0; i < 2; i++) {
            final int finalI = i;
            new Thread(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName() +" ---->"+sThreadLocal.get());
                    sThreadLocal.set(" runnable "+ finalI);
                    System.out.println(Thread.currentThread().getName() +" ---->"+sThreadLocal.get());

                }
            }).start();
        }
    }

看下输出结果:

ThreadLocal使用

当我们通过sThreadLocal在主线程存入变量main,在开启的子线程中是无法获取主线程存入的对象的,也就是说明了在当前线程存入的对象只在当前线程是有效的,不同线程间存入的数据得到隔离。

三、源码角度看看ThreadLocal原理

通常我们看到 ThreadLocal.set()方法时,最直观的感觉应该是觉得它是这么设计的:在ThreadLocal内部保存一个Map集合,使用当前线程Thread作为key,存入的对象作为value,以此来达到线程间数据隔离的目的。我最开始也是这么觉得的,当然,在JDK早期版本它也是这样的实现方式,但如果翻看JDK1.3、1.4、1.5之后的都已经不是这样设计的了。

那它又是如何实现的呢?开始我们的源码之旅吧,我们基于JDK1.8来看:

    /**
     * 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类型变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据当前ThreadLocal  查找对应的ThreadLocalMap.Entry
            //后面我们继续看下getEntry(this)
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果ThreadLocalMap.Entry不为空,返回内部存放的对象值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果get之前没有存入任何值,调用该方法
        return setInitialValue();
    }
  //
  /**
     * 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;
    }
  /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        //初始化变量的值   我们可以重写initialValue()方法设置默认初始值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

既然存储的对象值是在Thread中,那我们继续看下:

ThreadLocalMap
//ThreadLocalMap类
// table数组中存储了所有的存储对象的Entry内部存储了所有变量
 private Entry getEntry(ThreadLocal key) {
            //根据哈希码求出所在数组下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else//如果为空
                return getEntryAfterMiss(key, i, e);
        }
/**
     * 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)//向ThreadLocalMap存入对应的值
            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
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

总结一下:根据源码我们可以发现,我们存入的对象实际被存储在Thread内部的ThreadLocalMap变量内,以key和value的形式存储,ThreadLocal为key,传入的对象为value,我们可以在initialValue方法中设置默认值。

这样就和我们最开始的想法是不一样的了,那为什么JDK要这样设计,它有什么好处呢?

  1. 每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,提高性能。
  2. 当Thread销毁之后对应的内部变量ThreadLocalMap也就随之销毁,内存可以自动释放,减少内存使用。

我们继续往下看,来看看这个ThreadLocalMap内部存储的Entry:

 static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

注意,这里的Entry是将ThreadLocal作为弱引用来存储的,这可能会出现问题:如果一个ThreadLocal没有外部强引用引用它,那么系统GC的时候,这个ThreadLocal会被回收掉,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程不结束,那这些无用的强引用就始终无法回收,从而造成内存泄漏。

所以之前说到,建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry中的value值。

OK,到这里基本介绍就结束了,作为自己学习的一个记录,也希望对大家有所帮助。一切真相都在源码里。

android进阶之路
Web note ad 1