ThreadLocal详解

目录

1.ThreadLocal是什么?

2.为什么要使用ThreadLocal

3.ThreadLocal实现线程隔离原理

4.代码中如何实现的呢?

4.1 ThreadLocal的set方法

4.2 ThreadLocal的get方法

4.3 ThreadLocal的remove方法

5.Entry的key为什么设计成弱引用?

5.1 key为弱引用时

5.2 key为强引用时

5.3 小总结

6.ThreadLocal会造成内存泄露吗?

如何解决内存泄露

7.ThreadLocal使用场景


1.ThreadLocal是什么?

ThreadLocal翻译是线程本地类,这个类提供了 thread-local variables也就是线程本地变量(文章中以后简称线本量),线本量和普通的变量不同的地方在于,每一个访问(通过线本量的get和set方法访问)线本量的线程都有自己的,独立的已经初始化了的线本量的副本,简单讲就是:每一个访问线本量的线程都在本地内存维护了一份线本量的副本,多个线程操作线本量时,其实是对自己线程内部的副本进行操作。

一句话概括:ThreadLocal提供了一个线程隔离的线本量,避免线程并发场景下的线程安全问题。

注:ThreadLocal官方建议:ThreadLocal变量通常是私有,静态

2.为什么要使用ThreadLocal

在并发场景下的多线程修改共享变量时,会产生线程安全问题。

为了解决线程安全问题,可以使用加锁的方式,例如使用synchronized或Lock,但是加锁的方式会导致性能问题,加锁示意图如下:

还有一种解决方案,就是使用ThreadLocal,当多线程访问线本量时,会在线程本地内存维护线本量的副本,多线程操作线本量时其实是操作的自己线程内部的线本量副本,达到线程隔离的作用,如下图所示:

从内存空间占用和时间消耗来看,使用synchronized加锁的方式是用时间换空间:只有一个共享变量,占用空间小,但每次访问时都要排队等候别人访问;使用ThreadLocal方式是用空间换时间:每个线程都有一个变量的副本,占用空间大,但访问时不需要排队。

3.ThreadLocal实现线程隔离原理

每个线程在各自线程内部维护了一个线本量的副本,这个副本存储的位置就是Thread类内部一个ThreadLocal.ThreadLocalMap类型的变量 threadLocals,通过类名我们发现threadLocals其实是一个map(其实该map内部维护了一个Entry数组来存储键值对,源码解析时会讲),每个线程实例都有自己的map,当我们给每个线程设置独有的值时,其实是给这个map中写入键值对,从而实现线程隔离。

4.代码中如何实现的呢?

Thread类中有一个ThreadLocal.ThreadLocalMap类型的threadLocals属性,初始值为null(懒加载),这个threadLocals是一个map,它其实是由ThreadLocal维护的。

ThreadLocal中定义了静态内部类ThreadLocalMap就是保存每个线程独有的变量值的容器,具体的键值对是存入ThreadLocalMap中的Entry[]类型的table属性中(327行),他是一个Entry的数组,这里使用数组的原因:每个线程都会维护自己map,而每个线程可能会有多个线程本地变量,例如userId,transctionId,orderId等,他们都要存入的该map中,所以Entry数组就是用来存多个键值对。

讲完了Thread,ThreadLocal.ThreadLocalMap,ThreadLocal.ThreadLocalMap.Entry[]结构,我们来看一下代码如何实现

4.1 ThreadLocal的set方法

set方法入参就是变量值value,首先获取当前线程实例(200行),通过getMap方法获取ThreadLocalMap对象map(201行),getMap方法其实就是获取线程t的属性threadLocals(ThreadLocal.ThreadLocalMap类型),因为threadLocals默认为null,第一次调用时走205行的createMap的逻辑创建map,新建Entry对象,将ThreadLocal实例作为key,value作为value放入Entry对象中,并将Entry对象放入map的Entry数组中,然后将线程的属性threadLocals指向该新建的map。如果map已经创建不为空了,那么直接将ThreadLocal实例作为key,value作为value放入map对象中去。

4.2 ThreadLocal的get方法

首先获取当前线程实例(160行),根据线程实例获取线程属性threadLocals赋值给map(161行),判断map是否为空,不为空说明该map初始化过,通过getEntry方法从map中获取Entry,其实质是从map中的Entry数组table中获取存储Entry对象,该方法使用ThreadLocal变量和Entry数组的长度-1进行与运算来获取Entry在Entry数组的位置。如果Entry为空返回null(走setInitialValue方法170行),如果不为空返回Entry对象中的变量值。

4.3 ThreadLocal的remove方法

ThreadLocal的remove方法其实调用的ThreadLocalMap中的remove方法,通过传入的ThreadLocal实例,从map中找到对应的Entry,然后将ThreadLocal实例指向null(498行 e.clear()),并且将Entry指向null(499行expungeStaleEntry(i)方法)。

5.Entry的key为什么设计成弱引用?

首先我们分析一下设计成弱引用有什么好处

5.1 key为弱引用时

ThreadLocal对象有两处引用,第一个是Entry中的ThreadLocal变量的弱引用,第二个是MyClass.threadLocal的强引用(ThreadLocal官方建议将其定义为静态私有属性),当设置MyClass.threadLocal指向null,MyClass.threadLocal指向ThreadLocal对象的引用就消失了,ThreadLocal对象只有一条弱引用指向自己,那么当jvm发生GC时,可以无视弱引用,ThreadLocal对象就可以被回收。

ThreadLocal对象被回收后,threadLocalMap中就存在key为null,值为value的Entry,如果线程不结束(比如线程池)这时Entry中的value会有一条强引用链:thread变量-->thread对象-->threadLocalMap对象-->Entry-->value对象,链上的对象不能被回收,造成内存泄露。

5.2 key为强引用时

如果ThreadLocal变量设计成强引用,即便是MyClass.threadLocal指向null,此时ThreadLocal对象还有一条强引用指向自己,当jvm发生GC也不能够回收ThreadLocal对象。

如果线程不结束,ThreadLocal对象和强引用链上的对象都不会被回收。

5.3 小总结

实际上,ThreadLocalMap的开发团队已经考虑到这种情况,加了一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有keynullvalue

当MyClass.threadLocal指向null

如果key为强引用,那么ThreadLocal对象还有一条key的强引用,ThreadLocal对象就不能被GC回收,即使执行了get,set,remove方法,Entry中的value也不能被回收,结果就是:ThreadLocal对象以及Entry中的value都不能被回收。

如果key为弱引用,那么ThreadLocal对象就会被GC回收,如果执行了get,set,remove方法Entry中value就能被回收,结果就是:ThreadLocal对象能被回收,Entry中的value可能被回收。

结论:key为弱引用明显比强引用多了一层保障,更有利于ThreadLocal对象以及Entry的value值被回收。

6.ThreadLocal会造成内存泄露吗?

ThreadLocal变量对象不会造成内存泄露,它想要被回收时只需要让MyClass.threadLocal=null即可(如果不是静态变量,同样也是让变量指向null即可)

ThreadLocalMap会造成内存泄露,当ThreadLocal的强引用指向了null,在发生GC时就会将ThreadLocal对象清理掉,这是Entry中的key指向了null,但是value值却还在内存中,如果程序中不再执行set,get,remove方法,或者线程对象一直不结束,那么ThreadLocalMap中的Entry将造成内存泄露。

如何解决内存泄露

使用try...finally块进行回收,在finally块中手动执行remove方法即可将entry中的key和value全部置空。

阿里java开发手册:必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收。

objectThreadLocal.set(userInfo);
​
try {
    // ...
}  finally {
    objectThreadLocal.remove();
}

7.ThreadLocal使用场景

1,用户登录成功后,页面每次请求都会带token+用户信息,添加拦截器,在preHandle方法中将token+用户信息存入到ThreadLocal变量中,并提供工具类方法,方便在该请求的任何位置获取token+用户信息,并在afterCompletion方法中执行remove操作(只要preHandle放行,无论是否异常,以及拦截器链后续的拦截是否通过都会执行afterCompletion方法),此时用户信息就好比上下文(Context)一样,不需要传递,直接获取。

2,对于像SimpleDateFormat这种线程不安全的类,并发场景下,多线程使用一个共享对象会出现错误,但是又不可能在每一个线程中new一个SimpleDateFormat对象,此时使用ThreadLocal现场本地变量,并重写initialValue方法让每个线程重新new一个SimpleDataFormat对象。

3,ThreadLocal在Spring框架中的实际应用(和场景1几乎是一样的),Spring-security框架的核心组件SecurityContextHolder,重点是将给定的认证信息(SecurityContext)与当前执行线程关联。也就是说在同一个线程中可以通过该组件随时方便的获得认证信息,我们看一下SecurityContextHolder的源码。

// 保存认证信息
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
​
// 获取认证信息
SecurityContextHolder.getContext();
public class SecurityContextHolder {
    // 认证信息的上下文SecurityContext其实是成员变量strategy在维护,set,get方法也是操作strategy。
    private static SecurityContextHolderStrategy strategy;
    
    public static SecurityContext getContext() {
        return strategy.getContext();
    }
    
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }
}

SecurityContextHolderStrategy是个接口有多个实现类,默认是ThreadLocalSecurityContextHolderStrategy,我们看下其源码:

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
    
    @Override
    public void clearContext() {
        contextHolder.remove();
    }
​
    @Override
    public SecurityContext getContext() {
        SecurityContext ctx = contextHolder.get();
        if (ctx == null) {
            ctx = createEmptyContext();
            contextHolder.set(ctx);
        }
        return ctx;
    }
​
    @Override
    public void setContext(SecurityContext context) {
        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    }
}

解读源码:ThreadLocalSecurityContextHolderStrategy类有一个成员变量ThreadLocal<SecurityContext> contextHolder,他就是认证信息上下文的主要实例对象,通过ThreadLocal将认证信息上下文securityContext和当前线程绑定在了一起,在当前线程中可以随时通过该组件访问认证信息。不同的用户的认证信息绑定在不同的线程,实现了认证信息的线程隔离。

4,使用ThreadLocal的典型场景是数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用

5,Spring声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。

注:4,5两种方式在很多文章中看到,还没去源码验证。

参考:面试必备:ThreadLocal详解 - 掘金

声明:侵权必删,本文无任何功利,给知识传播多一扇窗。

  • 28
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值