深入理解ThreadLocal

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容