ThreadLocal详解

302 阅读6分钟

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

一、什么是ThreadLocal

ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突,线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。

线程安全的解决思路:
1)互斥同步:synchronized 和 ReentrantLock
2) 非阻塞同步:CAS 、AtomicXXX
3) 无同步方案:本地存储(Thread Local)


本地存储(Thread Local)官网解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code 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) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

ThreadLocal提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal变量通常被private static修饰。当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收。
总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。


二、ThreadLocal原理-如何实现线程隔离

每个Thread对象都有一个ThreadLocalMap,当创建一个ThreadLocal时,就会将该Threadlocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类似。
向ThreadLocal存入一个值,实际上是向当前线程对象中的ThreadLocalMap存入值,ThreadLocalMap可以简单的理解成一个Map,而向这个Map存值的key就是ThreadLocal实例本身。\

image.png

image.png


也就是说,想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到当前线程中的一个Map中去了,获取ThreadLocal的值同样也是这个道理,就也就是ThreadLocal可以实现线程之间隔离的原因。

三、ThreadLocalMap对象是什么

image.png

image.png


1)每个Thread线程内部都有一个ThreadLocalMap;
2) Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value);
3) Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向Map获取和设置线程的变量值;
4)一个Thread可以有多个ThreadLocal;
总之,每个线程都有其独有的Map结构,而Map中存有的是ThreadLocal为key变量副本为Vaule的键值对,以此达到变量隔离的目的。

当初始化一个线程时其内部去创建一个ThreadLocalMap的Map容器待用

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

当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;
        }
}

线程Thread内部的Map容器初始化完毕,那么它又是如何和ThreadLocal有关系,ThreadLocal又是如何管理键值对的关系。

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

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

当在Thread内部调用set()方法时:
1)第一步会获取调用当前方法的线程Thread;
2) 获取当前线程内部的ThreadLocalMap容器;
3)把变量副本给set到Map;
ThreadLocal(就是个维护线程内部变量的工具),只是在set时去操作Thread内部的ThreadLocalMap将变量拷贝到Thread内部的Map容器中,key就是当前的ThreadLocal,value就是变量的副本。


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)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

1)获取当前线程的ThreadLocalMap对象;
2) 从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点;
3)从Entry节点获取存储的对应value副本值返回;
4)map为空的话返回初始值null,即线程变量副本为null;

remove()方法移除当前线程的副本变量值:

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * <tt>initialValue</tt> method in the current thread.
 *
 * @since 1.5
 */
public void remove() {
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

 /**
  * Remove the entry for key.
  */
    private void remove(ThreadLocal<?> key) {
    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)]) {
              if (e.get() == key) {
                  e.clear();
                  expungeStaleEntry(i);
                  return;
              }
          }
    }

四、内存泄漏问题

实际上ThreadLocalMap中使用的key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收时必然会被清理掉。

key的弱引用问题:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key

生命周期长:线程池中的线程

ThreadLocal在没有外部对象引用时如Thread,发生GC时弱引用key会被回收,而value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄漏。

1)key如果使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

2)key使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove时会被清除。

Java8中已经做了一些优化如,在ThreadLocal的get()、set()、remove()方法调用时会清除掉线程ThreadLocalMap中所有Entry中key为null的value,并将整个Entry设置为null,利于下次内存回收。

建议回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally块进行回收。