ThreadLocal的介绍+经典应用场景

16,451 阅读8分钟

🏅作者小三是刚刚毕业不久全栈工程师,写的技术文章基本上是学习过程中笔记整理而来,大家看了之后如果喜欢可以给小弟点点赞哦。
🏅例外小弟还有个程序员交流群,欢迎各位大佬来摸鱼哈。点击加群

什么是ThreadLocal

ThreadLocal又叫做线程局部变量,全称thread local variable,它的使用场合主要是为了解决多线程中因为数据并发产生不一致的问题。ThreadLocal为每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果无非是耗费了内存,也大大减少了线程同步所带来的性能消耗,也减少了线程并发控制的复杂度。

总的来说:ThreadLocal适用于每一个线程需要自己独立实例,而且实例的话需要在多个方法里被使用到,也就是变量在线程之间是隔离的但是在方法或者是类里面是共享的场景

那ThreadLocal和Synchronized又有什么区别呢?

虽然ThreadLocal和Synchonized都用于解决多线程的并发访问,但是它们之间还是会有一些本质上的区别的:

Synchronized是利用锁的机制,使得变量或者是代码块在某一时刻里只能被一个线程来进行访问。ThreadLocal是为每一个线程都提供了一个变量的副本,这样就是的每一个线程在某一时刻里访问到的不是同一个对象,这样就隔离了多个线程对数据的数据共享,Synochronized正好相反,可以用于多个线程之间通信能够获得数据共享。

注:ThreadLocal不可以使用原子类型,只能使用Object类型

ThreadLocal的简单使用

public class ThreadLocaTest {
 
    private static ThreadLocal<String> local = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中变量的值
        System.out.println(str + " :" + local.get());
        //清除内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaTest.local.set("xdclass_A");
                print("A");
                //打印本地变量
                System.out.println("清除后:" + local.get());  
            }
        },"A").start();
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaTest.local.set("xdclass_B");
                print("B");
                System.out.println("清除后 " + localVar.get());
            }
        },"B").start();
    }
}

 

运行后可以看到xdclass_A的值为null,xdclass_B的值也为null,表明了两个线程都分别获取了自己线程存放的变量,他们之间获取到的变量不会错乱。

ThreadLocal核心应用的场景介绍

ThreaLocal作用在每个线程内都都需要独立的保存信息,这样就方便同一个线程的其他方法获取到该信息的场景,由于每一个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息之后,后续方法可以通过ThreadLocal可以直接获取到,避免了传参,这个类似于全局变量的概念。比如像用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名

如上图所示,就好比如线程A的方法一创建了变量A,方法二是跟方法一在同一个线程内,那么创建的变量A就是共享的。

#用户微服务配置token解密信息传递例子
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
                LoginUser loginUser = new LoginUser();
                loginUser.setId(id);
                loginUser.setName(name);
                loginUser.setMail(mail);
                loginUser.setHeadImg(headImg);
                threadLocal.set(loginUser);
            
后续想直接获取到直接threadLocal.getxxx就可以了

如何使用ThreadLocal来解决线程安全的问题

在我们平常的SpringWeb项目中,我们通常会把业务分成Controller、Service、Dao等等,也知道注解@Autowired默认使用单例模式。那有没有想过,当不同的请求线程进来后,因为Dao层使用的是单例,那么负责连接数据库的Connection也只有一个了,这时候如果请求的线程都去连接数据库的话,就会造成这个线程不安全的问题,Spring是怎样来解决的呢?

在Dao层里装配的Connection线程肯定是安全的,解决方案就是使用ThreadLocal方法。当每一个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果值为null,那就说明没有对数据库进行连接,连接后就会存入到 ThreadLocal里,这样一来,每一个线程都保存有一份属于自己的Connection。每一线程维护自己的数据,达到线程的隔离效果。

ThreadLocal慎用的场景

第一点(线程池里线程调用ThreadLocal):因为线程池里对线程的管理都是线程复用的方法,所以在线程池里线程非常难结束,更有可能的是永远不会结束。这就意味着线程的持续时间是不可估测的,甚至会与JVM的生命周期一致。

第二点(在异步程序里):ThreadLocal的参数传递是不可靠的,因为线程将请求发送后,不会在等待远程返回结果就继续向下运行了,真正的返回结果得到以后,可能是其它的线程在处理。

第三点:在使用完ThreadLocal,推荐要调用一下remove()方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。

轻松的掌握ThreadLocal底层源码解读+原理

ThreadLocal的set方法,首先Thread t =Thread.currentThread意思就是获取到当前的线程,紧接着就是获取到线程当中的属性ThreadLocalMap,然后会进行对ThreadLocalMap进行判断,如果不为空,就直接更新要保存的变量值,否则的话就创建一个threadLocalMap,并且赋值。

那么ThreadLocalMap这个方法又是做什么的呢?接下来我们来看一看,可以看出ThreadLocalMap是一个ThreadLocal的内部静态类,这个类的构成主要是用Entry来保存数据,而且还是继承的弱引用。在Entry内部里使用ThreadLocal作为key,这里会使用我们自己设置的value作为value

上面说完set和ThreadLocalMap方法了,接下来我们再来看看get方法是怎样的。可以看出来get的方法和set的方法很类似,也是首先获取到当前的线程,再接着获取到线程的ThreadLocalMap,然后对map来进行判断。如果map的数据为空,那么就获取存储的值。如果数据为null的话,就开始进行初始化,初始化的结果就是Theradlocalmap存放的值为null。

可以看出,基本都操作都有这个ThreadLocalMap,这个类没有实现map的接口,就是一个普通的java类,但是实现的类就类似于map的功能,数据用Entry存储,Entry继承于WeakReference,用一个键值对来存储,键就是ThreadLocal的引用。每一个线程都有一个ThreadLocalMap的对象,每一个新的线程Thread都会实例化一个ThreadLocalMap并赋予值给成员变量threadLocals。

【面试题】为什么ThreadLocal的键是弱引用,如果是强引用会有什么问题呢?

什么是弱引用呢?(小白请看,大佬请略过~)

在java里,除了基础的数据类型以外,其他的都为引用类型,而java根据生命周期的长短又把引用类型分为强引用、软引用、弱引用和虚引用。正常的情况下我们平时基本上只适用到了强引用的类型,而其他的引用类型也就在面试中或者阅读源码的时候才能看到。

强引用:像new了一个对象就是强引用 Object obj = new Object()

软引用的话,生命周期会比强引用短一些,是通过SoftReference类实现的,当内存有足够的空间,那么垃圾回收器就不会回收它;因为当JVM认为内存空间出现不足的时候,就会尝试回收软引用指定的对象,就是说在JVM会在抛出OutOfMemoryError这个异常之前,会清理软引用对象。

软引用的使用场景:比较适合用来实现缓存,当内存空间充足的时候,将缓存存放到内存当中,如果内存不足了就可以把缓存回收掉

弱引用:弱引用就是通过WeakReference类来实现的,它的生命周期比软引用还要短(一个比一个短),在进行垃圾回收的时候,不管内存的空间够不够都会回收掉这对象

使用场景:如果一个对象只是偶尔来使用的话,希望在使用的时候能够随时的获取,但是呢,也不想影响到该对象的垃圾收集,这时候就可以考虑到使用弱引用来指向这个对象。

讲了这么多还是没有讲到这个面试题,现在就来讲讲这个面试题该怎么样回答

ThreadLocal为什么是WeakReference呢?

第一、如果是强引用的话,即使ThreadLocal的值是为null,但是的话ThreadLocalMap还是会有ThreadLocal的强引用状态,如果没有手动进行删除的话,ThreadLocal就不会被回收,这样就会导致Entry内存的泄漏

第二、如果是弱引用的话,引用ThreadLocal的对象被回收掉了,ThreadLocalMap还保留有ThreadLocal的弱引用,即使没有进行手动删除,ThreadLocal也会被回收掉。value在下一次的ThreadLocalMap调用set/get/remove方法的时候就会被清除掉。