什么,你的ThreadLocal内存泄漏了?
微信公众号:IT一刻钟。大型现实非严肃主义现场,一刻钟与你分享优质技术架构与见闻,做一个有剧情的程序员。 关注可第一时间了解更多精彩内容,定期有福利相送哟。
又是一个风和日丽的早上。
这天小美遇到了一个难题。
原来小美在做用户服务鉴权的时候,需要根据每个请求获取token:
//获取认证信息 Authentication authentication = tokenProvider.getAuthentication(jwt); //设置认证信息 SecurityContext.setAuthentication(authentication);
然后经过层层的调用,在业务代码里根据认证信息进行权限的判断,也就是鉴权。
小美心里琢磨着,如果每个方法参数中都传递SecurityContext信息,就显的太过冗余,而且看着也丑陋。
那么怎么才能隐式传递参数呢?
这个当然难不倒小美,她决定用ThreadLocal来传递这个变量:
class SecurityContextHolder { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<SecurityContext>(); public SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { contextHolder.set(createEmptyContext()); } return ctx; } } ......(省略不必要的) SecurityContextHolder.getContext().setAuthentication(authentication);
整体思路上就是将SecurityContext放入ThreadLocal,这样当一个线程缘起生灭的时候,这个值会贯穿始终。
完美,小美喜滋滋的提交了代码,然后发布出去了。
结果第二天系统就出现异常了,明明是这个用户A的发起的请求,到了数据库中,却发现是操作人是用户B的信息,一时间权限大乱。
完蛋了。。。
这是为什么呢?
我们得先扯一扯ThreadLocal,Thread,ThreadLocalMap之间的爱恨情仇。
图片解说:
1.Thread即线程,内部有一个ThreadLocal.ThreadLocalMap,key值是ThreadLocal,value值是指定的变量值;
2.ThreadLocalMap内部有一个Entry数组,用来存储K-V值,之所以是数组,而不是一个Entry,是因为一个线程可能对应有多个ThreadLocal;
3.ThreadLocal对象在线程外生成,多线程共享一个ThreadLocal对象,生成时需指定数据类型<?>,每个ThreadLocal对象都自定义了不同的threadLocalHashCode;
4.ThreadLocal.set 首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,并存放数据于Entry[]中;
5.ThreadLocal.get 首先根据当前线程Thread找到对应的ThreadLocalMap,然后将ThreadLocal的threadLocalHashCode转换为ThreadLocalMap里的Entry数组下标,根据下标从Entry[]中取出对应的数据;
6.由于Thread内部的ThreadLocal.ThreadLocalMap对象是每个线程私有的,所以做到了数据独立。
于是我们知道了ThreadLocal是如何实现线程私有变量的。 但是问题来了,如果线程数很多,一直往ThreadLocalMap中存值,那内存岂不是要撑死了?
当然不是,设计者使用了弱引用来解决这个问题:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
不过这里的弱引用只是针对key。每个key都弱引用指向ThreadLocal。当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被GC回收。
然而,value不能被回收,因为当前线程存在对value的强引用。只有当前线程结束销毁后,强引用断开,所有值才将全部被GC回收,由此可推断出,只有这个线程被回收了,ThreadLocal以及value才会真正被回收。
听起来很正常?
那如果我们使用线程池呢?常驻线程不会被销毁。这就完蛋了,ThreadLocal和value永远无法被GC回收,造成内存泄漏那是必然的。
而我们的请求进入到系统时,并不是一个请求生成一个线程,而是请求先进入到线程池,再由线程池调配出一个线程进行执行,执行完毕后放回线程池,这样就会存在一个线程多次被复用的情况,这就产生了这个线程此次操作中获取到了上次操作的值。
怎么办呢?
解决办法就是每次使用完ThreadLocal对象后,都要调用其remove方法,清除ThreadLocal中的内容。 示例:
public class ThreadLocalTest { static ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(0)); static class Task implements Runnable { @Override public void run() { int initial = sequencer.get().getAndIncrement(); // 期望初始为0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); } }
输出:
0
1
0
2
3
1
这里就是错误的。 如果每次执行完调用remove:
@Override public void run() { int initial = sequencer.get().getAndIncrement(); // 期望初始为0 System.out.println(initial); sequencer.remove(); }
输出:
0
0
0
0
0
0
输出则正常。
好了,本期就说到这里,转发加关注,是我分享的最大动力~

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
JavaScript"模拟事件"的注意要点
今天小编就为大家分享一篇关于JavaScript"模拟事件"的注意要点详解,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。 DOM中的事件模拟 三个步骤: 首先通过document.createEvent()方法创建event对象,接收一个参数,即表示要创建的事件类型的字符串: UIEvents(DOM3中的UIEvent)鼠标和键盘事件; MouseEvents(DOM3中的MouseEvent)鼠标事件; MutationEvents(DOM3中的MutationEvent)变动事件; HTMLEvents(没有DOM3中对应的事件)HTML事件; 其次在创建了event对象之后,还需要使用与事件有关的信息对其进行初始化。每种类型的event对象都有一个特殊的方法,为它传入适当的数据就可以初始化该event对象。用event.init......()此类行的方法。 最后就是触发事件。这需要使用dispatchEvent()方法,接收一个参数,即表示要触发的event对象。 模拟鼠标事件 首先创建鼠标事件对象的方法createEv...
- 下一篇
Spring MVC请求处理流程分析
一、简介 Spring MVC框架在工作中经常用到,配置简单,使用起来也很方便,很多书籍和博客都有介绍其处理流程,但是,对于其原理,总是似懂非懂的样子。我们做技术,需要做到知其然,还要知其所以然。今天我们结合源码来深入了解一下Spring MVC的处理流程。 以上流程图是Spring MVC的处理流程(参考:spring-mvc-flow-with-example),原作者对流程的解释如下: Step 1: First request will be received by DispatcherServlet. Step 2: DispatcherServlet will take the help of HandlerMapping and get to know the Controller class name associated with the given request. Step 3: So request transfer to the Controller, and then controller will process the request by ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- Mario游戏-低调大师作品
- CentOS关闭SELinux安全模块
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS6,CentOS7官方镜像安装Oracle11G
- Hadoop3单机部署,实现最简伪集群
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作