ThreadLocal从使用到实现原理与源码详解

ThreadLocal概述

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal 同时实现了线程内的资源共享。

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

ThreadLocal基本使用

三个主要方法:

  • set(value) 设置值

  • get() 获取值

  • remove() 清除值

代码示例:

public class ThreadLocalTest {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("kjz");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t1").start();
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            threadLocal.set("fw");
            print(name);
            System.out.println(name + "-after remove : " + threadLocal.get());
        }, "t2").start();
    }

    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + threadLocal.get());
        //清除本地内存中的本地变量
        threadLocal.remove();
    }

}

Thread的使用场景

场景一:代替参数的显式传递

当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

这个场景其实使用的比较少,一方面显式传参比较容易理解,另一方面我们可以将多个参数封装为对象去传递。

场景二:全局存储用户信息

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)

对于笔者而言,这个场景使用的比较多,当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

public class AuthNHolder {
	private static final ThreadLocal<Map<String,String>> loginThreadLocal = new ThreadLocal<Map<String,String>>();

	public static void map(Map<String,String> map){
		loginThreadLocal.set(map);
	}
	public static String userId(){
    		return get("userId");
	}
	public static String get(String key){
    		Map<String,String> map = getMap();
    		return map.get(key);
    }
	public static void clear(){
       loginThreadLocal.remove();
	}
	
}

场景三:解决线程安全问题

在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?

在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题

ThreadLocal在设计之初就是为解决并发问题而提供一种方案,每个线程维护一份自己的数据,达到线程隔离的效果。

慎用的场景

1.线程池中线程调用使用ThreadLocal 由于线程池中对线程管理都是采用线程复用的方法。在线程池中线程非常难结束甚至于永远不会结束。这将意味着线程持续的时间将不可预測,甚至与JVM的生命周期一致

2.异步程序中,ThreadLocal的参数传递是不靠谱的, 由于线程将请求发送后。就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是其他的线程。Java8中的并发流也要考虑这种情况

3.使用完ThreadLocal ,最好手动调用 remove() 方法,防止出现内存溢出,因为中使用的key为ThreadLocal的弱引用, 如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,但是如果value是强引用,不会被清理, 这样一来就会出现 key 为 null 的 value。

ThreadLocal的实现原理&源码解析

我们先从ThreadLocal的set方法入手:

进一步跟进set方法:

我们读源码可以看到:

Thread 类有个 ThreadLocalMap 成员变量,这个ThreadLocalMap中的Key是Threadlocal 对象,

value是要存放的线程局部变量。 set 也就是向当前线程的ThreadLocalMap中存放了一个元素(Entry),Key是ThreadLocal对象,value就是需要存入的业务数据。

ThreadlocalMap

这里需要注意 ThreadLocalMap是Thread 类的成员变量,而不是ThreadLocal中的。Thread类中有个成员变量ThreadlocalMap,普通的Map,key存放的是Threadlocal对象,value是你要跟线程绑定的值(线程隔离的变量),比如这里是用户信息对象(order).

那肯定有人想问了,为什么ThreadLcoalMap要定义在Thread中?

  ThreaLocalMap是自定义的哈希映射,仅适用于维护线程局部值。 没有操作导出到ThreadLocal类之外。 该类是包私有的,以允许声明Thread类中的字段。 为了帮助处理非常长的使用寿命,哈希表条目使用WeakReferences作为键。 但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的条目。

为什么不用Thread当作KEY ?取数据不是更加方便吗?

不可以,因为一个线程是可以拥有多个私有变量的,如果现在是只有一个Order线程操作,如果再加一个别的消息(比如优惠券相关信息),那该怎么办?如果重新set就会把原先的内容给覆盖了,这意味着还点做点「手脚」来唯一标识set进去的value。假设上一步解决了,还有个问题就是:并发量足够大时,意味着所有的线程都去操作同一个Map,Map体积有可能会膨胀,导致访问性能的下降,这个Map维护着所有的线程的私有变量,意味着你不知道什么时候可以「销毁」。线程需要多个私有变量,那有多个ThreadLocal对象当作key足以,对应的Map体积不会太大,只要线程销毁了,ThreadLocalMap也会被销毁.

如果在ThreaLocalMap中重新添加一个第二个元素,只需要 重新创建一个 private ThreadLocal<CardInfo> cardInfoThreadLocal = new ThreadLocal<>(); 就可以了,这样当前线程就会有优惠券信息了。

ThreadlocalMap的数据结构

class ThreadLocalMap {
 //初始容量
 private static final int INITIAL_CAPACITY = 16;
 //存放元素的数组
 private Entry[] table;
 //元素个数
 private int size = 0;
}

table 就是存储线程局部变量的数组,数组元素是Entry类,Entry由key和value组成,key是Threadlocal对象,value是存放的对应线程变量。

ThreadLocalMap发生Hash冲突怎么解决?

ThreadLocalMap 采用的是开放定址法,如果发生冲突,就往后找相邻的下一个节点,如果相邻的节点是空的,那么久直接存进去,如果不为空,继续往后查找,如果找到数据的最后也没有找到空的,就扩容

源码如下:

private void set(ThreadLocal<?> key, Object value) {
  Entry[] tab = table;
  int len = tab.length;
  // hashcode & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要存放元素的数组下标
  int i = key.threadLocalHashCode & (len-1);

  //找到数组的空槽(=null),一般ThreadlocalMap存放元素不会很多
  for (Entry e = tab[i];
       e != null; //找到数组的空槽(=null)
       e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

    //如果key值一样,算是更新操作,直接替换
    if (k == key) {
      e.value = value;
      return;
    }
  //key为空,做替换清理动作,这个后面聊WeakReference的时候讲
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
 //新new一个Entry
  tab[i] = new Entry(key, value);
  //数组元素个数+1
  int sz = ++size;
  //如果没清理掉元素或者存放元素个数超过数组阈值,进行扩容
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

//顺序遍历 +1 到了数组尾部,又回到数组头部(0这个位置)
private static int nextIndex(int i, int len) {
  return ((i + 1 < len) ? i + 1 : 0);
}

// get()方法,根据ThreadLocal key获取线程变量
private Entry getEntry(ThreadLocal<?> key) {
  //计算hash值 & 操作其实就是 %数组长度取余数,例如:数组长度是4,hashCode % (4-1) 就找到要查询的数组地址
  int i = key.threadLocalHashCode & (table.length - 1);
  Entry e = table[i];
  //快速判断 如果这个位置有值,key相等表示找到了,直接返回
  if (e != null && e.get() == key)
    return e;
  else
    return getEntryAfterMiss(key, i, e); //miss之后顺序往后找(链地址法,这个后面再介绍)
}

ThreadLocal-内存泄露问题

问题阐述

Java对象中的四种引用类型:强引用、软引用、弱引用、虚引用

强引用:最为普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收

弱引用:表示一个对象处于可能有用且非必须的状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收

每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

如果ThreadLocal没有外部强引用(但是这个概率是非常低的,我们知道Thread在创建的时候,会有栈引用指向Thread对象,Thread对象内部维护了ThreadLocalMap引用),那么在发生垃圾回收的时候,ThreadLocal就必定会被回收,而ThreadLocal又作为Map中的key,ThreadLocal被回收就会导致一个key为null的entry,外部就无法通过key来访问这个entry,垃圾回收也无法回收,这就造成了内存泄漏。

解决方案

解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

编程小猹

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

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

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

打赏作者

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

抵扣说明:

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

余额充值