10.ThreadLocal

97 阅读6分钟

一、什么是ThreadLocal?为什么要使用它?

概念:ThreadLocal即线程本地变量,每个线程都会有这个变量的一个本地拷贝副本,多个线程操作该变量时,实际是在操作自己本地内存里的变量,起到了线程隔离的作用,避免了线程安全问题。

作用:并发场景下会存在多个线程同时修改一个共享变量的场景,这就可能出现线程安全问题。为了解决这一问题,我们可以使用加锁的方式(synchronized\Lock)。但是加锁的方式可能导致系统变慢,还有另一种方案,就是使用空间换时间的方式,使用ThreadLocal。

二、ThreadLocal原理

2.1 ThreadLocal内存结构

image.png

Thread类中维护了ThreadLocal.ThreadLocalMap的成员变量,ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象的值。

2.2 源码分析性

2.2.1 Thread类中维护ThreadLocalMap

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

2.2.2 ThreadLocalMap源码

static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //Entry数组
    private Entry[] table;
    
    // ThreadLocalMap的构造器,ThreadLocal作为key
    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);
    }
}

2.2.3 ThreadLocal set()

 public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
    }

2.2.4 ThreadLocal get()

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) { //如果获取的ThreadLocalMap对象不为空
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue(); //初始化threadLocals成员变量的值
    }
    
     private T setInitialValue() {
        T value = initialValue(); //初始化value的值
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
        if (map != null)
            map.set(this, value);  //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //实例化threadLocals成员变量
        return value;
    }
  • Thread线程类中维护一个ThreadLocal.ThreadLocalMap的实例变量,每个线程都有个属于自己的ThreadLocalMap
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值
  • 并发场景下,每个线程Thread在往ThreadLocal里面设置值的时候,都是往自己的ThreadLocalMap里存,读取也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

三、问题

3.1 为什么不直接用线程id作为ThreadLocalMap的key呢?

例如:

public class TianLuoThreadLocalTest {
    private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
}

一个使用类中有两个ThreadLocalMap类型的共享变量,如果用线程id作为ThreadLocalMap的key就无法区分哪个ThreadLocal成员变量,因为他们线程id都是一样的。每个ThreadLocal对象,都可以由threadLocalHashCode属性唯一区分的,每个ThreadLocal对象都可以由这个对象名字唯一区分

image.png

3.2 弱引用导致的内存泄漏

ThreadLocal引用示意图:

image.png 关于ThreadLocal内存泄漏,网上比较流行的说法是这样的:

ThreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value;如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏

ThreadLocal变量被手动设置为null后的引用链图: image.png

3.3 key是弱引用,GC回收会影响ThreadLocal的正常工作吗?

弱引用:具有弱引用的对象拥有更短暂的生命周期,如果一个对象只有弱引用存在,则下次GC将会回收掉该对象 结论:不会影响,因为有ThreadLoca变量引用它,是不会被GC回收的,除非手动把ThreadLocal变量设置为null。

3.4 Enter的key为什么要设计成弱引用?

我们先来回忆一下四种引用:

  • 强引用:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
  • 软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
  • 弱引用:具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)。
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

image.png 下面我们分情况讨论:

  • 如果Key使用强引用:当ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏问题。
  • 如果Key使用弱引用:当ThreadLocal的对象被回收了,因为ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

由此可以发现使用弱引用作为Entry的key可以多一层保证;弱引用ThreadLocal不会轻易内存泄漏

实际上内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用Entry有两种方式:

  • 一种是使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
  • 另外一种就是:ThreadLocalMap的自动清除机制去清除过期Entry(ThreadLocalMap的get(),set(时都会差法过期Entry的清除))

四、ThreadLocal使用场景和注意点

  • ThreadLocal使用完要手动调用remove()
  • 使用日期工具类,用到SimpleDateFormat,使用ThreadLocal保证线程安全
  • 全局存储用户信息(用户信息存入ThreadLocal,那么当前线程在任何地方需要时,都可以使用)
  • 保证同一个线程,获取的数据库连接Connection是同一个,使用ThreadLocal来解决线程安全问题

参考: