ThreadLocal详解

Posted 耶瞳

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal详解相关的知识,希望对你有一定的参考价值。

如果有兴趣了解更多相关内容,欢迎来我的个人网站看看:瞳孔空间

一:基本介绍

ThreadLocal类能提供线程内部的局部变量。这种变量在多线程环境下访问时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

归纳要点,即:

  • 线程并发:在多线程并发的场景下
  • 传递数据:我们可以通过ThreadLocal在同一线程的不同组件中传递公共变量
  • 线程隔离:每个线程的变量都是独立的,不会互相影响

二:基本使用

ThreadLocal有以下四个常用方法:

方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove移除当前线程绑定的局部变量

现在有如下使用场景:

  • 线程A:设置变量1,获取变量1
  • 线程B:设置变量2,获取变量2

代码如下:

/**
 * @author eyes
 * @date 2023/1/21 9:15
 */
@Data
public class Demo 
  // 变量
  private String content;

  public static void main(String[] args) 
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) 
      new Thread(() -> 
        demo.setContent(Thread.currentThread().getName());
        System.out.println("");  // 打印这个是为了让线程在setContent之后不立即执行下面的输出,让访问错乱的效果更明显
        System.out.println(Thread.currentThread().getName() + "----->" + demo.getContent());
      , "线程" + i).start();
    
  


可见如果不将线程隔离,那么多线程并发场景下就会导致错乱,为此可以使用ThreadLocal进行改进:

/**
 * @author eyes
 * @date 2023/1/21 9:15
 */
public class Demo 
  ThreadLocal<String> tl = new ThreadLocal<>();

  private String getContent() 
    return tl.get();
  

  private void setContent(String content) 
    tl.set(content);
  

  public static void main(String[] args) 
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) 
      new Thread(() -> 
        demo.setContent(Thread.currentThread().getName());
        System.out.println("");  // 打印这个是为了让线程在setContent之后不立即执行下面的输出,让访问错乱的效果更明显
        System.out.println(Thread.currentThread().getName() + "----->" + demo.getContent());
      , "线程" + i).start();
    
  

三:源码解析

3.1:内部结构

如果我们不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal确实是这样设计的,如下图:


但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值object。具体的过程是这样的:

  • 每个Thread线程内部都有一个Map(ThreadLocalMap)
  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

两者对比可知,JDK8的设计方案有如下好处:

  • 在实际生产环境中,ThreadLocal数往往少于Thread数,因此该方案的每个ThreadLocalMap存储的Entry数量变少,减少了哈希冲突,效率更高。
  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的占用

3.2:核心方法源码

基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。ThreadLocalMap是ThreadLocal的静态内部类,由于内容较多,因此ThreadLocalMap单独放到3.3中介绍。

除了构造方法外,ThreadLocal对外暴露的方法有以下4个:

方法声明描述
protected initialValue()返回当前线程局部变量的初始值
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove移除当前线程绑定的局部变量

3.2.1:set

    /**
     * 设置当前线程对应的ThreadLocal的值
     *
     * @param 将要保存在当前线程对应的ThreadLocal的值
     */
    public void set(T value) 
    	// 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null) 
        	// 存在则调用map.set设置此实体entry
            map.set(this, value);
         else 
        	// 当前线程Thread不存在ThreadLocalMap对象则创建该对象
        	// 并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        
    

    /**
     * 获取当前线程Thread对应维护的ThreadLocalMap
     *
     * @param t 当前线程
     * @return 对应维护的ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) 
        return t.threadLocals;
    


    /**
     * 创建当前线程Thread对应维护的ThreadLocalMap
     *
     * @param t 当前线程
     * @param firstValue 存放到map中的第一个entry的值
     */
    void createMap(Thread t, T firstValue) 
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    

3.2.2:get


    /**
     * 返回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量,则调用initialvalue方法进行初始化值
     * 
     * @return 当前线程对应此ThreadLocal的值
     */
    public T get() 
    	// 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) 
        	// 以当前的ThreadLocal为key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) 
                @SuppressWarnings("unchecked")
                // 获取存储实体e对应的value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            
        
        // 调用initialvalue方法进行初始化值
        // 有两种情况会执行当前代码
        // 1. map不存在,表示此线程没有维护的ThreadLocalMap对象
        // 2. map存在,但是没有与当前ThreadLocal关联的entry
        return setInitialValue();
    


    /**
     * set()方法的变种,用以构建初始化值 
     * 当set()方法被重写时用以替代原set()方法
     *
     * @return 初始化值
     */
    private T setInitialValue() 
    	// 调用initialValue获取初始化的值
    	// 此方法可以被子类重写,如果不重写则默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) 
        	// map存在则调用map.set()设置此实体类
            map.set(this, value);
         else 
        	// map不存在则调用createMap进行ThreadLocalMap对象的初始化
        	// 并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        
        if (this instanceof TerminatingThreadLocal) 
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        
        return value;
    

3.2.3:remove


    /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() 
     	 // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) 
         	 // map存在则删除对应entry
             m.remove(this);
         
     

3.2.4:initialValue


    /**
     * 返回当前线程对应的ThreadLocal的初始值
     * 当线程没有先调用set方法就调用get方法时,此方法才会执行
     * 
     * 这个方法仅仅简单返回null,如果程序员想ThreadLocal线程局部变量有一个
     * 除null以外的初始值,必须通过子类继承的方式去重写此方法,通常可以用匿名内部类实现
     * 该方法是protected,显然是为了让子类覆盖而设计的
     * 
     * @return 当前ThreadLocal的初始值
     */
    protected T initialValue() 
        return null;
    

匿名内部类重写initialValue方法:

  ThreadLocal tl = new ThreadLocal() 
    @Override
    protected String initialValue() 
      return "瞳孔";
    
  ;

除此之外,ThreadLocal还提供了一个便捷的静态方法:

ThreadLocal<String> tl = ThreadLocal.withInitial(() -> "瞳孔");

这样的话就可以优雅地设置初始值了:

3.3:ThreadLocalMap

ThreadLocalMap是ThreadLocal的静态内部类,虽然它叫map,但并没有实现Map接口,它用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。

3.3.1:成员变量


        /**
         * 初始容量 -- 必须是2的整次幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的table, 大小会根据需要调整
         * 数组长度必须是2的整次幂
         */
        private Entry[] table;

        /**
         * 数组里entrys的个数,可以用于判断table当前使用量是否超过阈值
         */
        private int size = 0;

        /**
         * 进行扩容的阈值,表使用量大于它的时候进行扩容
         * 默认为0
         */
        private int threshold;

3.3.2:存储结构 - Entry

ThreadLocalMap用Entry来保存K-V结构数据。不过Entry的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。

另外,Entry继承WeakReference,也就是key(Threadlocal)是弱引用,其目的是将Threadlocal对象的生命周期和线程生命周期解绑。


        /**
         * Entry继承WeakReference,并且用ThreadLocal作为key
         * 如果key为null(entry.get() == null),意味着key不再被引用,此时entry可以从table中清除
         */
        static class Entry extends WeakReference<ThreadLocal<?>> 
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) 
                super(k);
                value = v;
            
        

3.3.3:内存泄漏问题

不了解JVM的话建议先看看JVM垃圾回收再看,推荐这篇:JVM详解——垃圾回收

内存泄漏相关概念:

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

Java中的引用有4种类型:强引用、软引用、弱引用和虚引用。当前这个问题只涉及强引用和弱引用。

  • 强引用(StrongReference):最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”。内存不足时,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
  • 软引用(SoftReference):软引用是用来描述一些有用但并不是必需的对象。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存,比如网页缓存、图片缓存等。
  • 弱引用(WeakReference):用来描述非必需对象,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用与软引用的区别在于,只具有弱引用的对象拥有更短暂的生命周期。软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
  • 虚引用(PhantomReference):和软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。

有时候使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。先看下面这张图,假设Entry用的是强引用:


假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了。但是因为ThreadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。在没有手动删除这个Entry以及CurrentThread依然运行的前提下,
始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。也就是说,ThreadLocalMap中的key使用了强引用,也是无法完全避免内存泄漏的。

下面是key使用弱引用的情况:

同样假设在业务代码中使用完ThreadLocal,ThreadLocal Ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向Threadlocal实例,所以Threadlocal就可以顺利被gc回收,此时Entry中的key=null。但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 ThreadRef->currentThread->threadLocalMap->entry ->value,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

因此我们可以知道,无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。

那么为什么key要用弱引用呢?事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set、get、remove中的任一方法的时候会被清除,从而避兔内存泄漏。

3.3.4:Hash冲突问题

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1,寻找下一个相邻的位置。

		private void set(ThreadLocal<?> key, Object value) 
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 // 在这里调用nextIndex选择数据存入的位置
                 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();
        

        private static int nextIndex(int i, int len) 
            return ((i + 1 < len) ? i + 1 : 0);
        

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

以上是关于ThreadLocal详解的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal相关

源码分析--ThreadLocal(图解)

转载解密ThreadLocal

Thread 与 ThreadLocal

ThreadLocal(线程绑定)

ThreadLocal源码分析_02 内核(ThreadLocalMap)