ThreadLocal深入了解

413 阅读6分钟

1.ThreadLocal 作用和用法

ThreadLocal 也叫局部变量,目的就是解决在多线程的时候,解决共享变量线程安全问题.通过将变量保存在线程局部,从而达到多线程之间独有一份,不产生竞争.

这个时候就会有一个疑问,那直接在线程内声明局部变量不就可以了?

直接到线程内定义局部变量确实可以解决线程安全问题,但是如果是夸类,方法等调用的时候,需要时时刻刻带着该变量,导致业务极度耦合,而使用ThreadLocal就可以很好的规避这个问题.

// ThreadLocal的本质其实ThreadLocal.ThreadLocalMap 而ThreadLocal只是起到一个桥接的作用

// 通常我们在使用ThreadLocal的时候,都会将其设置为static final,主要是为了可以让多线程共同访问,多次实例化ThreadLocal就违背了线程共享的本质
static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

public static void main(String[] args) {
    THREAD_LOCAL.set("我是主线程");

    // 通过THREAD_LOCAL到当前线程ThreadLocal.ThreadLocalMap去获取数据,意味着该数据是存储在当前线程内部的,从而实现线程隔离
    log.info(THREAD_LOCAL.get());

    // 在使用完以后,需要手动remove,避免不必要的内存泄露
    THREAD_LOCAL.remove();
    new Thread(() -> {
        // 这里获取的是子线程中ThreadLocal.ThreadLocalMap中的数据,ThreadLocal中的数据可以夸方法,类共享,但是不能夸线程共享
        THREAD_LOCAL.set("我是子线程");

        log.info(THREAD_LOCAL.get());

        THREAD_LOCAL.remove();
    }).start();

}

那为什么不通过方法参数传递变量?

这样会导致类与类之间的紧耦合,特别是在跨多个方法的时候

为什么 TheadLocal 常作为私有静态使用?

如果 TheadLocal 不作为静态变量,而属于某个线程的实例类,就失去了线程之间共享的本质属性,同时 ThreadLocal 的作用只是标识变量以及访问 ThreadLocalMap ,多份的 ThreadLocal 没有必要

2.TheadLocal 实践场景

1.透传全局上下文 2.解决并发下 SimpleDateFormat 的线程不安全 等问题

3.ThreadLocal的源码

  • ThreadLocalMap本质
static class ThreadLocalMap {

    /**
     * Entry继承了WeakReference,实现键弱应用
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        // 键是ThreadLocal本身,而值就是需要存储的值,将这个保存在Thread中
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    /**
     * 初始容量为16
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 要调整大小的下一个大小值
     */
    private int threshold; // Default to 0

    /**
     * 达到2/3的时候开始扩容
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
}

ThreadLocalMap本质是一个Entry对象,而Entry又继承了WeakReference,实现了键弱引用,而值还是强引用,初始容量为16,当容量达到2/3时,会开始扩容,ThreadLocalMap并不像hashMap一样,通过拉链法解决hash冲突问题,而是采用开放地址法解决hash冲突

  • set方法源码
public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}
  • get方法源码

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}
  • remove方法的实现

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

4.ThreadLocal内存泄露问题

因为线程到 ThreadLocalMap 再到 Entry 再到 局部变量存在强引用的关系,当线程不死亡也不对局部变量进行 remove 操作,局部变量是不会被回收的。

虽然在 TheadLocalMap 中,Entry 的 key 是由一个虚引用指向相应的 TheadLocal 对象,当 ThreadLocal 变量没有强引用指向它时,指向 ThreadLocal 会被下一次 GC 回收,但尽管如此对于 Entry 中 Value 还是存在强引用是不会被回收的。更何况 ThreadLocal 变量一般由 private static 修饰,生命周期至少不会随着线程结束而结束。

总之就是,线程中的局部变量 Value 无法在在该回收时自动回收,线程持续执行或者呆在线程程池中占用不必要的内存,导致内存泄漏

由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

虽然TheadLocalMap的键是弱应用 会被GC掉,但是value还是强引用无法被GC,从而导致内存泄露问题,更何况平时正确使用ThreadLocal姿势,都是static final进行修饰,导致ThreadLocal和类共生死,拉长其生命周期,所以并不会随着线程死亡还回收

这需要实际的时候使用完毕及时调用remove()方法避免内存泄漏。

5.ThreadLocal不支持父子线程共享

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)