ThreadLocal使用以及面试题解析

简述:

在Java并发中,如果对于某些对象并不需要做共享操作,而是希望每个线程把对应的对象复制一份到线程内,加上线程天然的隔离性,这样可以完美的避免多个线程抢夺操作同一个对象从而报错。
ThreadLocal就是为了这个场景而产生的。

ThreadLocal VS Synchronized

ThreadLocal和Synchronized都是为了保证多线程场景下的线程安全,但是两者也有着本质的区别。
ThreadLocal用于处理变量为不共享,其实现原理其实就是将某些对象纳入线程中,这样对于某个公共的变量,如果有十个线程需要操作该对象,每个对象都将该变量Copy一份放入线程内,配合线程天然的隔离性可以避免多个线程抢夺共享变量的问题。
Synchronized用于处理变量共享导致的线程不安全问题,通过Synchronized锁可以保证多线程的可见性、事务一致性、顺序性。简而言之,当你需要安全的处理多线程使用的共享变量时且需要线程之间该变量的互通(而不是简单的Copy副本各自处理)那么可以使用重量级锁Synchronized。

聊一下ThreadLocal实现原理

下图简单的反应一下:Thread、ThreadLocalMap、ThredLocal、Entry


ThreadLocal概貌图

查看Thread类会发现,在Thread类中有一个全局变量:

// ThreadLocalMap是ThreadLocal的一个静态内部类
ThreadLocal.ThreadLocalMap threadlocals;

那我们就顺着这个思路来聊一下原理,先聊一下ThreadLocalMap对象是如何挂载到线程类并且之后线程是如何获取对应相关联的ThreadLocalMap的。之后再去聊一下ThreadLocalMap内部的处理机制。

线程如何和ThreadLocalMap关联

在Thread类中有如下源码:


Thread.class

也就是说在线程内部有一个变量threadLocals。每个线程初始化时,该变量的默认值都为null。
那么ThreadLocalMap是何时以及如何会与Thread线程的threadLocals相关联呢?
其实这里也使用的是一种懒汉思想,也就是说,在Thread被创建之后,代码并不会自动的创建ThreadLocalMap对象并与Thread关联,而是在使用到线程中的ThreadLocal时才会去关联,比如,我们以threadLocal.set操作为例,投过源码分析:

@Slf4j
public class ThreadLocalTest {
    ThreadLocal<Object> threadLocals = new ThreadLocal<>(); // 往当前线程的ThreadLocal挂载
    @Test
    public void t() {
        threadLocals.set("1");
    }
}

在测试类中调用了set方法,断点跟踪一下,看看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);
    }

对上述set(T value)方法做说明:

  1. 首先获取当前线程并赋值给变量t
  2. 根据t变量调用本类的getMap方法用来获取ThreadLocalMap对象。继续瞅一眼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。

  1. 紧接着就跟了一个if分支,分两种情况,当map == null时候则内部调用createMap方法,如果map不为空,那么就直接调用ThreadLocalMap.set方法进行赋值操作(由于这个地方主要讲的是ThreadLocalMap和Thread的挂载问题,因此map.set(this,value)放到下文详细描述),主要看一下createMap(t,value)是如何创建ThreadLocalMap对象又如何挂载到Thread上的,看下面createMap源码:
    /**
     * 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对象并将其挂载到Thread类上,参数为ThreadLocal当前对象this和setValue方法中的Value方法(当然是这样,可以设想在第一次调用ThreadLocal.set方法的时候如果ThreadLocalMap为空则创建,创建完毕一定是需要紧接着存Value)。看到这里我们知道了ThreadMap和Thread是如何挂载的。
其实我只是拿ThreadLocal.set操作为例,其实同样ThreadLocal.get操作也同样先判断线程中的ThreadLocalMap是否为空,若不为空则会调用createMap的方式来进行创建。

ThreadLocalMap类

简述:在看完了ThreadLocalMap如何与Thread进行挂钩的,其实背后原理很简单,就是一个ThreadLocalMap对象被赋值给了Thread中的threadlocals变量。
所以最核心的代码其实都在ThreadLocal类和ThreadLocalMap类中(ThreadLocalMap为ThreadLocal的一个静态内部类)
我们还是以ThreadLocal.set(Object value)为例,来阐述,我们想要存入ThreadLocal的值是保存在哪的。

    /**
     * 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);
    }

这段代码熟悉吧?这个就是ThreadLocal.set方法,上文已经详细说明了createMap方法,现在来看一下map.set方法,先瞅一下源码:

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

需要注意的是:ThreadLocalMap他内部的本质其实是一个Entry[]数组,也就是说,在ThreadLocalMap中其实并没有使用ConcurrentHashMap等线程安全的相关数据结构,而是通过Entry数组结合Hash(key)&Entry.length的方式进行对Entry数组读写。
这一点在读源码的时候需要注意。
看一下ThreadLocal中对Entry[]的定义:

ThreadLocalMap下的Enrty数组定义

在回来看set核心代码,其他他就是先根据set的第一个参数:key(属于ThreadLocal)然后和Entry当前的容量做&操作。然后得到i变量并作为Enry的数组下标访问到Entry[i]中的Entry对象。这个Entry对象就是我们最终需要的,Entry的Key为ThreadLocal对象,Value为我们保存的Value。拿到这个最终的Entry之后我们就可以做相关的get和set操作了。
看了这么多,再去看一下上述文章的createMap方法中new ThreadLocalMap方法,看看在初始化的时候是如何决定将当前的ThreadLocal放入到Entry[]数组中的哪个下标的,看源码:

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

INITIAL_CAPACITY:16
其实这个地方的
threshold:INITIAL_CAPACITY * 2 / 3
INITIAL_CAPACITY是不是很熟悉,其实和Map数据结构中也有类似的字段。
而threshold就类似于Map中的负载因子了。而上述代码的ThreadLocal.set方法中,就有在某些场景下调用refresh方法,因为Entry是以数组出现的,所以自然而然的想到,这个地方的扩容其实和JDK中的ArrayList动态扩容是一个样子了。

面试题

  1. 在ThreadLocalMap中内部类Entry为何对ThreadLocal采用弱引用的方式?
    答:Entry对于ThreadLocal采用弱引用是为了更好的方便ThreadLocal的GC操作;
    在如下情况:当不想再去使用ThreadLocal的时候,正常情况下,我们可以将ThreadLocal的外部引用置为null,这样可以辅助下次GC的时候回收掉ThreadLocal变量。但是此刻如果对应的Thread一直处于运行状态,那么ThreadLocal存在于这样的一条强引用链:Thread -> ThreadLocalMap -> Entry -> ThreadLocal。因此对于ThreadLocal的两条强引用链中只要有一方没有断开,那么GC在多次也无法对ThreadLocal进行回收。在这样的情况下,在Entry中使用对ThreadLocal的弱引用,只要Java程序中将ThreadLocal引用置为null,那么该ThreadLocal将不再存在强引用关系,下次GC可以对ThreadLocal对象进行回收。

  2. ThreadLocalMap中的Entry对ThreadLocal采用了弱引用的方式方便GC,那为何还会出现内存泄漏的问题?是什么对象可能发生泄漏?如何解决的?
    答:在思考了第一个问题之后,会发现,ThreadLocal确实更加容易回收了,比如只要发生GC且用户程序中也没有对ThreadLocal进行强引用,那么ThreadLocal对象便会被回收。
    但问题是:在ThreadLocal被回收之后,Entry中就会存在这样的一对数据<null,value>,又因为存在如下强引用链,导致GC时Value无法被回收:Thread->ThreadLocalMap->Entry->Value;直到Thread线程终止。
    此时在编程上如果不加以特殊处理,那么这样的value值将永远无法被回收。ThreadLocal中采用的方法是:在set、get、remove方法中每一次操作都会手动将Entry中key为null的value也置为null,方便在下一次GC的时候进行回收。
    所以在释放ThreadLocal对象之前,最好先调用一次remove将value先清空掉,否则先释放了ThreadLocal对象则无法再调用ThreadLocal中的任何方法了。
    如下代码:

try {
 // 业务代码
} finally {
 threadLocal.remove();
 threadLocal = null;
}
  1. 在使用ThreadLocal过程中,在当前线程下创建子线程,子线程无法获取父线程的数据,如何解决?
    答:因为子线程对象和父线程对象肯定不是同一个,在ThreadLocal中根据Thread对象获取到的Entry对象自然也就不同。
    可以使用ThreadLocal的子类:InheritableThreadLocal;当一个线程进行创建子线程的过程中,父线程会将自身的InheritableThreadLocal变量中的数据全部传递给子线程的InheritableThreadLocal。因此子线程也可以使用父线程ThreadLocal中的数据了。见如下代码:
    private static ThreadLocal local = new ThreadLocal();
    private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();

    public static void main(String[] args) throws InterruptedException {
        local.set("ThreadLocal");
        inheritableThreadLocal.set("InheritableThreadLocal");
        new Thread(() -> {
            System.out.println(local.get());
            System.out.println(inheritableThreadLocal.get());
        }).start();
        Thread.sleep(20000);
    }

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