目录
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里所有key
为null
的value
。
当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两种方式在很多文章中看到,还没去源码验证。
声明:侵权必删,本文无任何功利,给知识传播多一扇窗。