ThreadLocal是什么?有哪些应用场景?

发布时间 2023-12-24 23:54:45作者: Crazy_Joker

大家好,我是joker,希望你快乐。

多线程情况下操作共享变量会产生线程安全问题,需要进行线程间同步,但是并不是所有的情况都是多线程去操作共享变量,有些线程是无状态的只进行操作处理,不涉及共享数据操作,所以就需要threadlocal登场了。

threadlocal是什么?

threadlocal根据命名来看,分为两部分:thread,local,线程,本地的,私有的。通过源码中的注释This class provides thread-local variables可以看出threadlocal这个类提供线程本地变量,作为一个线程内的全局变量。

threadlocal源码分析

下面通过代码进行分析,先来看一个类图:

通过类图可以看出,ThreadLocal依赖于ThreadLocalMap,Thread通过threadLocals属性与ThreadLocalMap组成1:1的组合关系。

Thread的私有数据存储在ThreadLocalMap内。

ThreadLocal中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);
        }
    }

ThreadLocal中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();
    }

Thread中的threadLocals属性:

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

可以看到ThreadLocalMap中维护了一个Entry类型数组,用于存储最终的数据,可以先把ThreadLocalMap认为是一个hashmap,还是有很多不同,下次再做比较。

简单做一个总结,Thread的私有变量是存储在自身上的,类型为一个ThreadLocal.ThreadLocalMap的变量,ThreadLocal是一个工具类,用于去维护线程的本地变量。

threadlocal的应用场景

通过上面的分析,可以看出ThreadLocal是一个线程安全,线程私有的,线程生命周期内的全局变量。

通过这些优势特点,可以看出ThreadLocal是线程安全的,避免某些情况需要考虑线程安全进行同步带来的性能损失。

线程生命周期内的全局变量,可以作为一个上下文信息,在整个线程的运行周期内共享数据。

threadlocal可能产生的问题,如何避免

map的key为ThreadLocal实例,key使用了弱引用,所以当把ThreadLocal实例设置为null后,没有任何强引用指向原来的ThreadLocal实例,所以ThreadLocal实例就可以顺利被GC回收。

仍然会造成内存泄漏问题,虽然key为弱引用类型,ThreadLocal能被及时回收,但是value却依然存在内存泄漏问题。ThreadLocal实例的引用设置为null后,没有任何强引用指向原来的ThreadLocal实例,ThreadLocal实例就可以被GC回收,ThreadLocal实例被回收后,value永远不会被访问到,所以存在内存泄漏问题。threadLocals对象中的Entry对象不再使用后,如果没及时清除Entry对象,而程序自身也无法通过垃圾回收机制自动清除,就可能导致内存泄漏。

  • 如何解决

1. 每次用完ThreadLocal都记得调用remove()方法清除数据

2. 将ThreadLocal变量尽可能的定义成static final

  • 内部优化:

1. 调用set()方法时,会采样清理,全量清理,扩容时还会继续检查

2. 调用get()方法时,没有直接命中,向后唤醒查找时会进行清理

3. 调用remove()方法时,除了清理当前Entry,还会向后继续清理