ThreadLocal详解

1.简介

​ 在项目开发中,有时候一个方法中设置或者初始化的值,在调用其它很多方法时,都需要使用此值,此时可以通过传参的方式一直传递下去,但这样增加了传递的复杂度和耦合度;aop在调用之前设置某些值,在调用结束或抛出异常时需要使用此值。对于这些场景,都可以使用ThreadLocal来实现。

​ ThreadLocal是用来操作线程Thread内部变量threadLocals的工具类,ThreadLocal设置变量、获取变量之前,都需要先获取当前线程Thread.currentThread(),然后操作当前线程的内部变量threadLocals进行存取,这样就能保证线程的变量独立于其它线程,对其它线程是隔离的,在自己线程内共享变量,在线程生命周期内起作用。

2.使用案例

(1)数据传递

​ 在方法中传递参数或者跨层传递参数,使用ThreadLocal存取值。

public class ThreadLocalDemo {

    public static void main(String[] args) {
        Demo1 demo1 = new Demo1();
        demo1.test("张三");
        //使用完成移除ThreadLocal
        ThreadLocalHolder.threadLocal.remove();
    }
}

class Demo1{
    public void test(String name){
        ThreadLocalHolder.threadLocal.set(name);
        System.out.println("demo1设置的值:"+name);
        Demo2 demo2 = new Demo2();
        demo2.test();
    }
}

class Demo2{
    public void test(){
        String name = ThreadLocalHolder.threadLocal.get();
        System.out.println("demo2取到值:"+name);
        Demo3 demo3 = new Demo3();
        demo3.test();
    }
}

class Demo3{
    public void test(){
        String name = ThreadLocalHolder.threadLocal.get();
        System.out.println("demo3取到值:"+name);
    }
}

//构造ThreadLocal的提供类
class ThreadLocalHolder{
    //定义类型为String的ThreadLocal
    public static final  ThreadLocal<String> threadLocal = new ThreadLocal<String>();
}

​ 输出结果:
在这里插入图片描述
(2)方法拦截

​ 拦截器输出服务接口响应时间,需要在拦截器中的preHandle()方法执行之前记录下时间,在afterCompletion()请求处理结束方法中使用当前时间减去开始时间来计算接口响应时间。

@Slf4j
@Component
public class HttpLogHandlerInterceptor implements HandlerInterceptor {

    //定义类型为LocalDateTime的ThreadLocal
    private final static ThreadLocal<LocalDateTime> startTimeLocal = new ThreadLocal<>();

    //接口被调用处理之前的拦截器
    @Override
    public boolean preHandle(@NotNull HttpServletRequest request,
                             @NotNull HttpServletResponse response,
                             @NotNull Object handler) {
        //当前时间设置到ThreadLocal中
        startTimeLocal.set(LocalDateTime.now());
        //输出日志信息
        log.info("starting request.url:{},client addr:{},host addr:{}",
                request.getRequestURL(), request.getRemoteAddr(), request.getLocalAddr());
        return true;
    }

    //请求处理结束之后的拦截器
    @Override
    public void afterCompletion(@NotNull HttpServletRequest request,
                                @NotNull HttpServletResponse response,
                                @NotNull Object handler, Exception ex) {
        if (startTimeLocal.get() == null) {
            return;
        }
        //计算处理的时间,startTimeLocal.get()获取到preHandle()方法中设置的时间
        Duration ts = Duration.between(startTimeLocal.get(), LocalDateTime.now());
        //移除ThreadLocal
        startTimeLocal.remove();
        //输出日志信息
        log.info("finished request.status code:{},takes:{}", response.getStatus(), ts);
    }
}

​ 输出结果:
在这里插入图片描述

3.Thread、ThreadLocal、ThreadLocalMap、Entry关系

​ ThreadLocal在jdk1.8前后有不同的构造,这里分开说。

(1)jdk1.8之前

​ 每个ThreadLocal维护一个ThreadLocalMap,用线程Thread作为ThreadLocalMap的key,要存储的对象作为ThreadLocalMap的value,Entry是ThreadLocalMap用于存放变量对象的实体。
在这里插入图片描述
(2)jdk1.8及之后

​ 每个Thread维护着一个ThreadLocalMap类型的threadLocals属性,从Thread的源码中有体现:
在这里插入图片描述
​ ThreadLocalMap是ThreadLocal的内部静态类,Entry是ThreadLocalMap的内部静态类,每个Entry对ThreadLocal有弱引用,从ThreadLocal的源码中有体现:
在这里插入图片描述
​ 对Thread内部的属性threadLocals的操作通过ThreadLocal来进行,threadLocals属性的类型为ThreadLocalMap,ThreadLocalMap具体存储方式为Entry实体,ThreadLocal存储在Entry[]的哪个索引下由ThreadLocal的哈希码决定。
在这里插入图片描述
jdk1.8优化的好处:

①减少ThreadLocalMap的Entry数量:jdk1.8之前是多个线程依附在一个ThreadLocal上,有多少个线程使用就有多少个Entry;jdk1.8及之后是多个ThreadLocal依附在一个线程上,有多少种ThreadLocal线程变量就有多少个Entry,一般一个方法内使用到的线程变量不会太多。实际项目中往往是线程数量多于ThreadLocal数量。

②当线程Thread销毁的时候,里面的ThreadLocalMap也会随之销毁,减少内存的使用。

4.ThreadLocal创建

​ 创建ThreadLocal的时候需要指定存放的变量类型,例如String、User等,通常使用private static final 进行修饰,当ThreadLocal创建后就是一个固定不变的工具类,它的threadLocalHashCode也是由创建的时候生成。创建方式:

    private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
    private static final ThreadLocal<User> threadLocal2 = new ThreadLocal<User>();

threadLocalHashCode是用于计算ThreadLocal放到Entry数组的索引位置。看threadLocalHashCode生成的源码:

//ThreadLocal指定存储类型T
public class ThreadLocal<T> {

    //threadLocalHashCode的生成,由final修饰,生成后不能改变
    private final int threadLocalHashCode = nextHashCode();

    //使用AtomicInteger保证原子操作的Integer类,线程安全的方式进行加减数
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    //这个值和斐波那契散列(黄金分割数)有关
    private static final int HASH_INCREMENT = 0x61c88647;

    //使用AtomicInteger线程安全的方式计算threadLocalHashCode值,加上HASH_INCREMENT为了让哈希码均匀的分布在2的n次方的数组中
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

从源码中可以看出创建ThreadLocal需要指定存储的类型T,threadLocalHashCode由创建ThreadLocal的时候生成,用final修饰,生成后不可变,否则没法在存取中对应到同一个Entry值。

5.set方法原理

​ set是添加数据到线程变量的方法,看下ThreadLocal的set源码

  //ThreadLocal的set方法
  public void set(T value) {
        //获取到当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的属性threadLocals
        ThreadLocalMap map = getMap(t);
        //当前线程的threadLocals属性不为空
        if (map != null)
            //把值存到ThreadLocalMap中,key为当前ThreadLocal,value为设置的T
            map.set(this, value);
        else
            //当前线程的threadLocals属性为空,默认也是为null,则创建一个ThreadLocalMap
            createMap(t, value);
    }

    //ThreadLocal的getMap方法
    ThreadLocalMap getMap(Thread t) {
        //返回当前线程的属性threadLocals
        return t.threadLocals;
    }

从源码可以看出set方法需要获取当前线程,再根据当前线程获取到它的属性ThreadLocalMap(默认为null),当ThreadLocalMap不为空,则把设置的值添加到ThreadLocalMap中,若是为空则为当前线程创建ThreadLocalMap并把此值保存进去。

看下ThreadLocal类的createMap方法源码

   //ThreadLocal类的createMap方法
   void createMap(Thread t, T firstValue) {
        //给Thread类的threadLocals属性赋上值,新建一个ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

从源码可以看出创建了ThreadLocalMap对象,并给当前线程的threadLocals属性赋值上新建的这个对象。

看下ThreadLocalMap类的创建方法源码

//ThreadLocalMap类方法
//Entry内部静态类,弱引用ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
    //内部变量具体值
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
}

//Entry数组的默认大小16
private static final int INITIAL_CAPACITY = 16;
//Entry数组,存放ThreadLocal和Value
private Entry[] table;
//Entry数组的大小
private int size = 0;
//Entry数组的扩容因子
private int threshold; 

//ThreadLocalMap类中的构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     //初始化Entry数组,默认容量为16
     table = new Entry[INITIAL_CAPACITY];
     //通过ThreadLocal的哈希码和数组的大小计算此ThreadLocal放在数组的哪个索引下,相当于取模的方式
     int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
     //ThreadLocal确认了存在Entry数组的索引,值为Entry对象
     table[i] = new Entry(firstKey, firstValue);
     //Entry数组大小为1
     size = 1;
     //设置数组扩容因子,当达到时进行扩容, threshold = len * 2 / 3;
     setThreshold(INITIAL_CAPACITY);
}

从源码中可以看出创建ThreadLocalMap时,初始化了Entry数组,默认容量为16,通过ThreadLocal的哈希码threadLocalHashCode和数组的大小计算此ThreadLocal放在Entry数组的哪个索引下,相当于取模的方式;ThreadLocal确认了存在Entry数组的索引,则创建Entry对象,并添加到对应的Entry数组中,Entry数组的值为当前ThreadLocal的弱引用+需要存储的线程变量值。

​ 看下当ThreadLocalMap存在时ThreadLocalMap类的map.set(this, value)源码

  //ThreadLocalMap类的set方法
   private void set(ThreadLocal<?> key, Object value) {
            //取到Entry数组
            Entry[] tab = table;
            //获取到数组的长度
            int len = tab.length;
            //通过当前ThreadLocal的哈希码和数组长度取模,得到此ThreadLocal存放在Entry数组的索引
            int i = key.threadLocalHashCode & (len-1);
            
            //使用线性探测法处理
            //先获取计算出来的索引位置Entry,若是不为空,则判断当前的ThreadLocal与这个索引下的ThreadLocal是否为同一个,为同一个则进行值的覆盖;若不是同一个,判断此索引下的Entry是否为(null,value)的情况,若是,则用当前的ThreadLocal和value替代(null,value);若当前计算的索引i下已经存在其他的Entry(哈希冲突),且不存在(null,value)的情况,则使用nextIndex(i, len)从i开始到len之间(往len方向没找到,会从索引为0的方向开始找)使用环式的方式找到Entry数组中值为空的索引,并把此索引赋值给i,此时e != null条件不满足,则退出for循环
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //获取Entry的弱引用对象ThreadLocal
                ThreadLocal<?> k = e.get();
                //同一个ThreadLocal,则进行值的覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //此不为空的Entry的ThreadLocal为空(null,value),则进行替代
                if (k == null) {
                    //把ThreadLocal为空的值进行替代
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //经过上面的for循环,也没有找到此ThreadLocal的存放位置(出现哈希冲突),此时i为Entry数组中为空的索引,直接把此值放到这个空位置
            tab[i] = new Entry(key, value);
            //Entry数组的大小加1
            int sz = ++size;
            //判断此时的Entry数组是否大于扩容因子,大于则进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
      }

从源码中可以看出set的流程为:通过当前ThreadLocal的哈希码和数组长度取模,得到此ThreadLocal存放在Entry数组的索引。使用线性探测法处理,先获取计算出来的索引位置i的Entry,若是不为空,则判断当前的ThreadLocal与这个索引下的ThreadLocal是否为同一个,为同一个则进行值的覆盖;若不是同一个,判断此索引下的Entry是否为(null,value)的情况,若是,则用当前的ThreadLocal和value替代(null,value);若当前计算的索引i下已经存在其他的Entry(哈希冲突),且不存在(null,value)的情况,则使用nextIndex(i, len)从i开始到len之间(往len方向没找到,会从索引为0的方向开始找)使用环式的方式找到Entry数组中值为空的索引,并把此索引赋值给i,此时e != null条件不满足,则退出for循环。此时i为Entry数组中值为空的索引,直接把此Entry值放到这个空位置。判断此时的Entry数组是否大于扩容因子,大于则进行扩容。

来看nextIndex环式寻找下一个索引的源码

 //ThreadLocalMap环式寻找下一个索引的方法
 private static int nextIndex(int i, int len) {
    //当前索引i是否还有下一个索引,有则返回下一个索引,没有则从0开始,形成环的方式
    return ((i + 1 < len) ? i + 1 : 0);
 }

6.get方法原理

​ get是从线程变量中获取到值的方法,看下ThreadLocal的get源码

 //ThreadLocal的get方法
 public T get() {
        //获取到当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的属性threadLocals
        ThreadLocalMap map = getMap(t);
        //当前线程的threadLocals属性不为空
        if (map != null) {
            //从ThreadLocalMap中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //Entry不为空
            if (e != null) {
                @SuppressWarnings("unchecked")
                //取出Enetry内的value值返回
                T result = (T)e.value;
                return result;
            }
        }
        //当前线程的threadLocals属性为空,则新建一个ThreadLocalMap;或者没找到此ThreadLocal的记录,添加一个Entry(ThreadLocal,null)到ThreadLocalMap
        return setInitialValue();
    }

从源码可以看出get方法需要获取当前线程,再根据当前线程获取到它的属性ThreadLocalMap(默认为null),当ThreadLocalMap不为空,则从ThreadLocalMap中获取Entry,Entry不为空,则把Entry的value返回。若是ThreadLocalMap为空,则为当前线程创建ThreadLocalMap;新建一个value为null的Entry(ThreadLocal,null)添加到ThreadLocalMap中。

来看ThreadLocal类的setInitialValue方法源码

 //ThreadLocal类的方法
 private T setInitialValue() {
        //初始化value为null
        T value = initialValue();
        //获取到当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的属性threadLocals
        ThreadLocalMap map = getMap(t);
        //当前线程的threadLocals属性不为空
        if (map != null)
            //把值存到ThreadLocalMap中,key为当前ThreadLocal,value为设置的null
            map.set(this, value);
        else
            //当前线程的threadLocals属性为空,默认也是为null,则创建一个ThreadLocalMap
            createMap(t, value);
        //返回value值
        return value;
  }

  //初始化value为null的方法
  protected T initialValue() {
       return null;
  }

从源码可以看出setInitialValue方法设置value为null,需要获取当前线程,再根据当前线程获取到它的属性ThreadLocalMap(默认为null),当ThreadLocalMap不为空,则把此null值和ThreadLocal添加到ThreadLocalMap中。若是ThreadLocalMap为空,则为当前线程创建ThreadLocalMap,创建的方法在set方法中已经做出过说明。

​ 来看当ThreadLocalMap存在时ThreadLocalMap类的map.getEntry(this)源码

//ThreadLocalMap的方法
 private Entry getEntry(ThreadLocal<?> key) {
    //通过ThreadLocal的哈希码和数组的大小计算此ThreadLocal放在数组的哪个索引下,相当于取模的方式
    int i = key.threadLocalHashCode & (table.length - 1);
    //ThreadLocal确认了存在Entry数组的索引,Entry数组的变量名是table,根据索引取到此Entry
    Entry e = table[i];
    //当取到的Entry不为空,且Entry的弱引用等于当前的ThreadLocal,表示就是此ThreadLocal对应的Entry
    if (e != null && e.get() == key)
        //直接返回此ThreadLocal对应的Entry
        return e;
    else
        //当ThreadLocal计算出来的索引值的Entry不是此ThreadLocal存放的Entry时,表示存放此ThreadLocal时出现了哈希冲突或者还没有存放过值,存的时候是使用线性探测法找到的索引,所以取的时候也使用线性探测法去找到索引,定位Entry
        return getEntryAfterMiss(key, i, e);
 }

从源码中可以看出getEntry的方法:通过当前ThreadLocal的哈希码和Entry数组长度取模,得到此ThreadLocal存放在Entry数组的索引,根据此索引从Entry数组(变量名是table)取到Entry对象,当取到的Entry不为空,且Entry的弱引用等于当前的ThreadLocal,表示就是此ThreadLocal对应的Entry,直接返回此Entry。当ThreadLocal计算出来的索引值的Entry不是此ThreadLocal存放的Entry时,表示存放此ThreadLocal时出现了哈希冲突或者还没有存放过值,存的时候是使用线性探测法找到的索引,所以取的时候也使用线性探测法去找索引,定位Entry。

​ 没找到ThreadLocal对应的Entry时,来看ThreadLocalMap类的getEntryAfterMiss源码

//ThreadLocalMap类的方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            //获取到Entry数组
            Entry[] tab = table;
            //Entry数组长度
            int len = tab.length;

            while (e != null) {
                //获取到Entry的弱引用ThreadLocal
                ThreadLocal<?> k = e.get();
                //等于当前的ThreadLocal,直接返回
                if (k == key)
                    return e;
                //检查是否存在(null,value)形式的Entry,存在则移除此Entry
                if (k == null)
                    //从Entry数组中移除索引位置为i的Entry
                    expungeStaleEntry(i);
                else
                    //使用线性探测法找到下一个索引
                    i = nextIndex(i, len);
                //获取新找到索引的Entry
                e = tab[i];
            }
            //在Entry数组中没找到此ThreadLocal对应的Entry,则放回null
            return null;
        }

从源码中可以看出getEntryAfterMiss的方法:使用线性探测法的方式进行索引查找,当找到此ThreadLocal对应的Entry则返回,在查找过程中,对(null,value)的Entry进行清除处理;当此ThreadLocal还没有进行过set值,直接调get值,从Entry数组中查找不到,直接返回null。

7.remove方法原理

​ remove方法是从Entry数组中移除当前ThreadLocal设置的值,看下ThreadLocal的remove源码

 //ThreadLocal类的remove方法
 public void remove() {
     //获取当前线程的属性threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //当前线程的threadLocals属性不为空
     if (m != null)
         //进行移除操作
         m.remove(this);
 }

从源码中可以看出ThreadLocal类的remove方法:获取当前线程,再根据当前线程获取到它的属性ThreadLocalMap(默认为null),当ThreadLocalMap不为空,则调用ThreadLocalMap的remove方法。

来看ThreadLocalMap类的remove源码

   //ThreadLocalMap类的方法
   private void remove(ThreadLocal<?> key) {
            //获取到Entry数组
            Entry[] tab = table;
            //Entry数组长度
            int len = tab.length;
            //通过ThreadLocal的哈希码和数组的大小计算此ThreadLocal放在数组的哪个索引下,相当于取模的方式
            int i = key.threadLocalHashCode & (len-1);
            //使用线性探测法处理
            //先获取计算出来的索引位置Entry,若是不为空,则判断当前的ThreadLocal与这个索引下的ThreadLocal是否为同一个,为同一个则把此Entry对ThreadLocal的弱引用置为null,Entry的value置为null,此Entry所在的索引位置设置为null;若当前计算的索引i下已经存在其他的Entry(哈希冲突),则使用nextIndex(i, len)从i开始到len之间(往len方向没找到,会从索引为0的方向开始找)使用环式的方式查找索引
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //Entry的弱引用ThreadLocal和此ThreadLocal相等,表示同一个
                if (e.get() == key) {
                    //Entry对ThreadLocal的弱引用设置为null:referent = null
                    e.clear();
                    //索引i位置Entry的value设置为null,此位置的Entry设置为null
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

从源码中可以看出ThreadLocalMap类的remove方法:通过当前ThreadLocal的哈希码和数组长度取模,得到此ThreadLocal存放在Entry数组的索引。使用线性探测法处理,先获取计算出来的索引位置Entry,若是不为空,则判断当前的ThreadLocal与这个索引下的ThreadLocal是否为同一个,为同一个则把此Entry对ThreadLocal的弱引用置为null,Entry的value置为null,此Entry所在的索引位置设置为null;若当前计算的索引i下已经存在其他的Entry(哈希冲突),则使用nextIndex(i, len)从i开始到len之间(往len方向没找到,会从索引为0的方向开始找)使用环式的方式查找索引。

8.哈希冲突及解决方案

​ 存放线程变量值使用的是Entry数组,当我们有多个ThreadLocal时,计算出来的存放索引位置就可能相同,导致哈希冲突的情况,ThreadLocal从两个方面来解决哈希冲突。

(1)ThreadLocal的哈希码生成加入斐波那契散列(黄金分割数)

​ 每个ThreadLocal创建时,都会生成一个哈希码,使用threadLocalHashCode字段记录,此哈希码生成后不会变,在生成此哈希码时使用了AtomicInteger保证threadLocalHashCode生成的原子性,HASH_INCREMENT= 0x61c88647这个值和斐波那契散列(黄金分割数)有关(32位整型上限2^31-1乘以黄金分割比例0.618的值2654435769,用有符号整型表示为-1640531527,去掉符号后16进制表示为0x61c88647),加上HASH_INCREMENT计算是为了让哈希码能均匀的分布在2的n次方的数组中,减少哈希冲突,这也是数组的容量需要为2的幂的原因。

(2)线性探测法找到存放索引

​ 当ThreadLocal计算出来的Entry数组索引已经存放着其他的记录,则出现哈希冲突。使用线性探测法处理,先获取计算出来的索引位置i的值,若是不为空,则判断当前的ThreadLocal与这个索引下的ThreadLocal是否为同一个;若不是同一个(哈希冲突),则使用nextIndex(i, len)从i开始到len之间(往len方向没找到,会从索引为0的方向开始找)使用环式的方式找到Entry数组中值为空的索引,并把此索引赋值给i。

9.内存泄露及解决方案

​ 内存溢出:Memory overflow 没有足够的内存提供申请者使用;

​ 内存泄露:Memory leak程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行减慢甚至系统崩溃等严重后果,内存泄露的堆积终将导致内存溢出。

​ 当我们使用完ThreadLocal时,没有调用remove方法,会导致Entry数组存放的值一直被堆积,Entry对ThreadLocal是弱引用,当使用完ThreadLocal,虚拟机会回收ThreadLocal,此时Entry的值就变为(null,value),虽然在get和set方法时,都对Entry的(null,value)进行处理,但还是会导致内存泄露的风险。特别是使用线程池的时候,线程的结束时间基本跟系统服务保持一致。

解决方案:

(1)使用完ThreadLocal,调用remove方法,清除对应的Entry;

(2)ThreadLocal源码做的优化,Entry对ThreadLocal为弱引用,当ThreadLocal使用完,在下次jvm进行GC时会回收弱引用的ThreadLocal,弱引用多了一层保障;此时对应Entry的值会变为(null,value),在get、set方法时会去校验key为null的情况,对这样的Entry进行覆盖、清除。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值