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 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中就会存在这样的一对数据,又因为存在如下强引用链,导致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

你可能感兴趣的:(ThreadLocal使用以及面试题解析)