ThreadLocal面试一网打尽,直接干源码

       好久不见啊,今天1024——程序员节嘛;本来今天是不打算创作一篇文章的,因为上周末去了杭州玩了两天,快要累死了,然后今天周一公司事情也可多,想躺平躺平;但是转念一想今天可是程序员节啊,我不表示表示哪多不好意思啊,晚上下班到家吃过饭后,就赶紧打开笔记本来干了。
      上周五的时候研究了ThreadLocal源码,自我感觉研究的还算比较透彻,那今天趁着1024正好给大家分享这方面的知识点吧。由于本人知识水平有限,在下面文章中那些不对的地方,恳请大家指出。大家一起进步;好了,废话少说,一起进步。

      1、ThreadLocal介绍

            1.1、官方介绍

                  ThreadLocal来提供线程内部局部变量。这种变量能够在多线程环境下,能够保证各个线程之间的线程变量互不干扰。ThreadLocal实例通常来说是private static类型的,用于关联线程和线程上下文切换。
                  我们能够得知ThreadLocal的作用:提供线程内部的局部变量,不同线程之间不会干扰,这种变量在线程生命周期内起作用,减少同一个线程多个函数和组件的公共变量传递复杂度。

    总结:

  • 传递变量: 在同一线程内,可以将公共变量存放进ThreadLocal中,然后可以在该线程中任意传递
  • 线程隔离: 每个线程的变量都是独立的,不会相互影响

            1.2、基本使用

            首先来看看几个常用的方法

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

使用Demo:

public static void main(String[] args) {

        //创建ThreadLocal对象
        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        
        //设置stringThreadLocal的值value
        stringThreadLocal.set("test ThreadLocal");
        
        //获取stringThreadLocal的值
        stringThreadLocal.get();
        
        //移除stringThreadLocal的值
        stringThreadLocal.remove();

    }

上面的例子比较简单,就是简单使用。下面我们来分析内部结构;

      2、ThreadLocal内部结构

            2.1、常见误解

                     在刚开始学习之前,我就进入一个大家也会常进的误解就是,认为每个ThreadLocal创建一个Map,然后key是Thread,map的value是我们要存的值,这样就达到了线程之间局部变量隔离效果,早期的ThreadLocal确实是这样设计的,但是在1.8之后就发生了改变,因为这样设计会有弊端——一个ThreadLocal管理一个map,然后key是Thread,当Thread较多时,产生Hash冲突的概率就会比较大,并且维护比较麻烦
 jdk1.8版本

            2.2、现在设计

                    在JDK1.8之后发生了改变,改变成了一个Thread管理一个map,map的key是ThreadLocal,value是ThreadLocal的值。

具体过程是这样的:

  • 每个Thread都负责管理一个Map
  • Map的key值是ThreadLocal,value是真正要存储的Object
  • Thread内部是靠ThreadLocal来维护,由ThreadLocal来负责set值和get值
  • 针对不同的Thread,访问的是不同的Map,那么线程的ThreadLocal就做到了线程隔离,互不干扰
    在这里插入图片描述

            2.3、这样设计好处

这样设计主要有两个以下优势:

  • 这样设计之后,那么对应的map值就会减少,发生hash冲突概率会减少;之前存储的数量由Thread决定,现在由ThreadLocal的数量决定;在现实环境中ThreadLocal要比Thread少。
  • 当Thread销毁之后,那么对应的ThhreadLocal也会被回收,减少内存使用。

      3、ThreadLocal核心源码

            3.1、set()方法

//ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;

/**
* set()方法
*/
public void set(T value) {
		
		//获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程所管理的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果已经有了ThreadLocalMap
    if (map != null) {
    		//设置ThreadLocalMap的值,key为ThreadLocal;value就是我们所要设置的值
        map.set(this, value);
    } else {
    	//没有ThreadLocalMap则去创建
        createMap(t, value);
    }
}

/**
* 获取ThreadLocalMap的方法
*/
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

private void set(ThreadLocal<?> key, Object value) {

		//获取Thread中的ThreadLocalMap的数组
      Entry[] tab = table;
      int len = tab.length;
      //计算出该key应该存放数组中的位置——key的HashCode对(length-1)与运算
      int i = key.threadLocalHashCode & (len-1);
		
	  //利用线性探测法来解决hash冲突——当应该存放位置有值时,那么就存放该位置的下一个位置;
	  //如果下一个位置还有值时,那么存放下下一个,以此类推,当最后一个还不满足,
	  //那么就会去第0个寻找,可以理解为它就是一个循环数组
      for (Entry e = tab[i];
           e != null;
           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();
  }

/**
* 创建ThreadLocalmap
*/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/**
* ThreadLocalmap构造方法
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
		
	//创建一个length = 16的数组
	//长度和hashMap一样,必须为2的n次方
    table = new Entry[INITIAL_CAPACITY];
    //将key求出的HashCode对(length-1)与运算,(长度为2的n次方的原因在这里就体现出来了)
    //得到的值即为该ThreadLocal要存放该数组的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //将数组中该位置的值设置为该value
    table[i] = new Entry(firstKey, firstValue);
    //数组中存在值的长度
    size = 1;
    //可以理解为阈值;阈值等于leng*2/3,可以理解为和HashMap的加载因子一样,和它的加载机制也一样
    //ThreadLocalMap当存满 > 2/3*length时,会扩容为2倍
    setThreshold(INITIAL_CAPACITY);
}

/**
* 求出ThreadLocal的阈值
*/
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

执行流程:

  1. 求出该Thread
  2. 取出该Thread管理的ThreadLocalMap
  3. 如果ThreadLocalMap不为空,则将ThreadLocal作为key,传的Value作为值;
  4. 如果为空,则去初始化个ThreadLocalMap;并将key,value存放进去
注意:源码的注释中写的比较详细,建议大家好好看看会理解比较透彻

            3.2、get()方法


//get方法源码
public T get() {
	//求出该线程
    Thread t = Thread.currentThread();
    //获取该Thread管理的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	//Map不为空则获取出该ThreadLocal对应的数组元素
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
        	//如果元素不为null,则取出该元素对应的value值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //当map为空或者元素e为空时,会初始化
    return setInitialValue();
}
/**
 1. 初始化
*/
private T setInitialValue() {
	//调用initialValue获取初始化的值
    T value = initialValue();
    //获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    	//在则调用map.set设置此实体entry
        map.set(this, value);
    } else {
    		// 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 返回设置的值value
    return value;
}

执行流程:

  1. 首先获取当前线程, 根据当前线程获取一个Map
  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4
  3. 如果e不为null,则返回e.value,否则转到4
  4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
 总结:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

      今天先写到这吧,明天还要搬砖,不敢使劲熬夜了。明天早上见。
2022年10月24日23:51:02

            3.3、remove()方法

public void remove() {
	//获取该Thread管理的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    //map不为Null
    if (m != null) {
    	//删除该ThreadLocal对应的Entry
        m.remove(this);
    }
}

执行流程:

  1. 查找到Thread对应的ThreadLocalMap
  2. 删除对应ThreadLocal

      4、ThreadLocalMap核心源码

            通过上面我们了解到线程局部变量存储数据的不是ThreadLocal,而是ThreadLocalMap,key是对应的ThreadLocal,value是传入的Object;一个Thread对应一个ThreadLocalMap;下面我们就来讲讲ThreadLocalMap的核心源码。

            4.1、ThreadLocalMap局部变量

        /**
         * The initial capacity -- MUST be a power of two.
         */
         //ThreadLocalMap默认大小,这里基本和HashMap差不多,它的长度同样必须为2的n次方
        private static final int INITIAL_CAPACITY = 16;

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

        /**
         * The number of entries in the table.
         */
         //数组里面存放个数,可以通过size是否大于阈值来决定ThreadLocalMap是否扩容
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
         //进行扩容阈值
        private int threshold; // Default to 0

这里和HashMap很相似;有初始容量INITIAL_CAPACITY(必须为2的n次方);table是一个数组Entry[],用来存放数据;size是数组中实际存放的数据元素;threshold是阈值,通过判断size是否大于阈值决定是否扩容。

            4.2、存储结构-Entry

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

      在ThreadLocalMap中,存储数据用的entry[],entry数据结构是k,v结构,并且key必须是ThreadLocal类型
      另外,entry继承了WeakReference,也就是key实现了弱引用,其目的是保证了ThreadLocalMap生命周期和Thread生命周期一样

            4.3、弱引用和内存结构

  • 强引用: 就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用: 垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
    ThreadLocal中内部引用(实线表示强引用,虚线表示弱引用)结构图如下所示:
    在这里插入图片描述       假设在业务代码中使用完ThreaLocal ,Thread Local Ref被回收;
           由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
           但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

出现内存泄漏真实原因:

  • Entry没有手动删除
  • Thread没有运行结束

解决方法:

  • 针对第一种情况,可以在使用完ThreadLocal后,采用手动remove()即可,就能避免内存泄漏。
  • 第二种情况比较难控制了,因为Thread生命周期和ThreadLocal相同生命周期。只有当Thread执行结束,ThreadLocalMap才能被垃圾回收掉。

内存泄漏根源: ThreadLocalMap生命周期和Thread生命周期一样长,没有手动释放删除Entry,造成内存泄漏。

            4.4、为什么使用弱引用

            当使用弱引用时,ThreadLocal被回收时,ThreadLocalMap的key会被置为null;并且ThreadLocalMap中set和get方法时,会判断key是否为null,如果key为null,会将Entry中置为null。

            这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

            4.5、如何解决hash冲突问题

            这里采用了线性探测的方法
主要步骤:

  • 先利用key的hashCode对(length-1)与运算

  • 计算出来的值即为要存放的位置,如果该位置有值了,判断该位置的key值是否与传来的key相等,如果相等,则直接将value替换成新的;不相等的话,则查找下一个位置,如果下一个位置为null,插入;不为null,如果key相等,则替换;如果key不相等,则继续查找下一个;循环往复这样,如果最后一个都没找到,那么会查找entry[0],你可以理解他为一个循环数组。

  • 如果该位置为null,则直接插入
    最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

          5、ThreadLocal和Synchroized对比

  • Synchronized: 它采用的是加锁同步的方式,只提供一个变量,保证只有线程排队执行;采用的是时间换取空间的思想;这样效率比较慢。

  • ThreadLocal: 采用的是空间换取时间方式,每个线程都存放一份局部变量,从而实现不同的访问互不影响。
    总结: Synchronized是多线程间同步,ThreadLocal是多线程间相互隔离,ThreadLocal并发高。

            好了,这篇文章就到这吧,由于时间比较紧,也没有好好打磨,有写的不对还请多多指出。
            大家一起分享,一起加油,奥利给。大家一键三连哦。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岭岭颖颖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值