首页 文章 精选 留言 我的

精选列表

搜索[SpringBoot4],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 这 4 种 ThreadLocal 你都知道吗?

点击蓝色字关注我们! 什么是ThreadLocal ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal, 每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。 实际应用 实际开发中我们真正使用ThreadLocal的场景还是比较少的,大多数使用都是在框架里面。最常见的使用场景的话就是用它来解决数据库连接、Session管理等保证每一个线程中使用的数据库连接是同一个。还有一个用的比较多的场景就是用来解决SimpleDateFormat解决线程不安全的问题,不过现在java8提供了DateTimeFormatter它是线程安全的,感兴趣的同学可以去看看。还可以利用它进行优雅的传递参数,传递参数的时候,如果父线程生成的变量或者参数直接通过ThreadLocal传递到子线程参数就会丢失,这个后面会介绍一个其他的ThreadLocal来专门解决这个问题的。 ThreadLocal api介绍 ThreadLocal的API还是比较少的就几个api我们看下这几个api的使用,使用起来也超级简单 privatestaticThreadLocal<String>threadLocal=ThreadLocal.withInitial(()->"java金融");publicstaticvoidmain(String[]args){System.out.println("获取初始值:"+threadLocal.get());threadLocal.set("关注:【java金融】");System.out.println("获取修改后的值:"+threadLocal.get());threadLocal.remove();} 输出结果: 获取初始值:java金融获取修改后的值:关注:【java金融】 是不是炒鸡简单,就几行代码就把所有api都覆盖了。下面我们就来简单看看这几个api的源码吧。 成员变量 /**初始容量,必须为2的幂*Theinitialcapacity--MUSTbeapoweroftwo.*/privatestaticfinalintINITIAL_CAPACITY=16;/**Entry表,大小必须为2的幂*Thetable,resizedasnecessary.*table.lengthMUSTalwaysbeapoweroftwo.*/privateEntry[]table;/***Thenumberofentriesinthetable.*/privateintsize=0;/***Thenextsizevalueatwhichtoresize.*/privateintthreshold;//Defaultto0 这里会有一个面试经常问到的问题:为什么entry数组的大小,以及初始容量都必须是2的幂?对于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源码里面都是使用 hashCode &( -1) 来代替hashCode% 。这种写法好处如下: 使用位运算替代取模,提升计算效率。 为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列。 set方法 publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);} set方法还是比较简单的,我们可以重点看下这个方法里面的ThreadLocalMap,它既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),肯定是有自己的key和value组成,我们根据源码可以看出它的key是其实可以把它简单看成是ThreadLocal,但是实际上ThreadLocal中存放的是ThreadLocal的弱引用,而它的value的话是我们实际set的值 staticclassEntryextendsWeakReference<ThreadLocal<?>>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;//实际存放的值Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}} Entry就是是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。我们再来看下这个ThreadLocalMap是位于哪里的?我们看到ThreadLocalMap 是位于Thread里面的一个变量,而我们的值又是放在ThreadLocalMap,这样的话我们就实现了每个线程间的隔离。下面两张图的基本就把ThreadLocal的结构给介绍清楚了。接下来我们再看下ThreadLocalMap里面的数据结构,我们知道HaseMap解决hash冲突是由链表和红黑树(jdk1.8)来解决的,但是这个我们看到ThreadLocalMap只有一个数组,它是怎么来解决hash冲突呢?ThreadLocalMap采用「线性探测」的方式,什么是线性探测呢?就是根「据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置」。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。 /***Incrementimodulolen.*/privatestaticintnextIndex(inti,intlen){return((i+1<len)?i+1:0);}/***Decrementimodulolen.*/privatestaticintprevIndex(inti,intlen){return((i-1>=0)?i-1:len-1);} 这种方式的话如果一个线程里面有大量的ThreadLocal就会产生性能问题,因为每次都需要对这个table进行遍历,清空无效的值。所以我们在使用的时候尽可能的使用少的ThreadLocal,不要在线程里面创建大量的ThreadLocal,如果需要设置不同的参数类型我们可以通过ThreadLocal来存放一个Object的Map这样的话,可以大大减少创建ThreadLocal的数量。伪代码如下: publicfinalclassHttpContext{privateHttpContext(){}privatestaticfinalThreadLocal<Map<String,Object>>CONTEXT=ThreadLocal.withInitial(()->newConcurrentHashMap(64));publicstatic<T>voidadd(Stringkey,Tvalue){if(StringUtils.isEmpty(key)||Objects.isNull(value)){thrownewIllegalArgumentException("keyorvalueisnull");}CONTEXT.get().put(key,value);}publicstatic<T>Tget(Stringkey){return(T)get().get(key);}publicstaticMap<String,Object>get(){returnCONTEXT.get();}publicstaticvoidremove(){CONTEXT.remove();}} 这样的话我们如果需要传递不同的参数,可以直接使用一个ThreadLocal就可以代替多个ThreadLocal了。如果觉得不想这么玩,我就是要创建多个ThreadLocal,我的需求就是这样,而且性能还得要好,这个能不能实现列?可以使用netty的FastThreadLocal可以解决这个问题,不过要配合使FastThreadLocalThread或者它子类的线程线程效率才会更高,更多关于它的使用可以自行查阅资料哦。 下面我们先来看下它的这个哈希函数 //生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。privatestaticfinalintHASH_INCREMENT=0x61c88647;/***Returnsthenexthashcode.*/privatestaticintnextHashCode(){returnnextHashCode.getAndAdd(HASH_INCREMENT);} 可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527.当我们使用0x61c88647这个魔数累加对每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂(数组的长度)取模,得到的结果分布很均匀。我们可以来也演示下通过这个魔数 publicclassMagicHashCode{privatestaticfinalintHASH_INCREMENT=0x61c88647;publicstaticvoidmain(String[]args){hashCode(16);//初始化16hashCode(32);//后续2倍扩容hashCode(64);}privatestaticvoidhashCode(Integerlength){inthashCode=0;for(inti=0;i<length;i++){hashCode=i*HASH_INCREMENT+HASH_INCREMENT;//每次递增HASH_INCREMENTSystem.out.print(hashCode&(length-1));System.out.print("");}System.out.println();}} 运行结果: 71451231018156134112907142128310172431613202729162330512192618152229411182507142128354249566361320273441485562512192633404754614111825323946536031017243138455259291623303744515818152229364350570 不得不佩服下这个作者,通过使用了斐波那契散列法,来保证哈希表的离散度,让结果很均匀。可见「代码要写的好,数学还是少不了」啊。其他的源码就不分析了,大家感兴趣可以自行去查看下。 ThreadLocal的内存泄露 关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题。首先我们需要知道什么是内存泄露? ❝ 在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。 ❞ ThreadLocal的内存泄露情况: 线程的生命周期很长,当 ThreadLocal没有被外部强引用的时候就会被 GC回收(给 ThreadLocal置空了): ThreadLocalMap会出现一个 key为 null的 Entry,但这个 Entry的 value将永远没办法被访问到(后续在也无法操作 set、get等方法了)。如果当这个线程一直没有结束,那这个 key为 null的 Entry因为也存在强引用( Entry.value),而 Entry被当前线程的 ThreadLocalMap强引用( Entry[] table),导致这个 Entry.value永远无法被 GC,造成内存泄漏。下面我们来演示下这个场景 publicstaticvoidmain(String[]args)throwsInterruptedException{ThreadLocal<Long[]>threadLocal=newThreadLocal<>();for(inti=0;i<50;i++){run(threadLocal);}Thread.sleep(50000);//去除强引用threadLocal=null;System.gc();System.runFinalization();System.gc();}privatestaticvoidrun(ThreadLocal<Long[]>threadLocal){newThread(()->{threadLocal.set(newLong[1024*1024*10]);try{Thread.sleep(1000000000);}catch(InterruptedExceptione){e.printStackTrace();}}).start();} 通过jdk自带的工具jconsole.exe会发现即使执行了gc 内存也不会减少,因为key还被线程强引用着。效果图如下: 针对于这种情况 ThreadLocalMap在设计中,已经考虑到这种情况的发生,你只要调用 了set()、get()、remove()方法都会调用 cleanSomeSlots()、expungeStaleEntry()方法去清除 key为 null的 value。这是一种被动的清理方式,但是如果 ThreadLocal的 set(),get(),remove()方法没有被调用,就会导致 value的内存泄漏。它的文档推荐我们使用 static修饰的 ThreadLocal,导致 ThreadLocal的生命周期和持有它的类一样长,由于 ThreadLocal有强引用在,意味着这个 ThreadLocal不会被 GC。在这种情况下,我们如果不手动删除, Entry的 key永远不为 null,弱引用也就失去了意义。所以我们在使用的时候尽可能养成一个好的习惯,使用完成后手动调用下 remove方法。其实实际生产环境中我们手动 remove大多数情况并不是为了避免这种 key为 null的情况,更多的时候,是为了保证业务以及程序的正确性。比如我们下单请求后通过 ThreadLocal构建了订单的上下文请求信息,然后通过线程池异步去更新用户积分,这时候如果更新完成,没有进行 remove操作,即使下一次新的订单会覆盖原来的值但是也是有可能会导致业务问题。如果不想手动清理是否还有其他方式解决下列? FastThreadLocal 可以去了解下,它提供了自动回收机制。 在线程池的场景,程序不停止,线程一直在复用的话,基本不会销毁,其实本质就跟上面例子是一样的。如果线程不复用,用完就销毁了就不会存在泄露的情况。因为线程结束的时候会 jvm主动调用 exit方法清理。 /***ThismethodiscalledbythesystemtogiveaThread*achancetocleanupbeforeitactuallyexits.*/privatevoidexit(){if(group!=null){group.threadTerminated(this);group=null;}/*Aggressivelynulloutallreferencefields:seebug4006245*/target=null;/*Speedthereleaseofsomeoftheseresources*/threadLocals=null;inheritableThreadLocals=null;inheritedAccessControlContext=null;blocker=null;uncaughtExceptionHandler=null;} InheritableThreadLocal 文章开头有提到过父子之间线程的变量传递丢失的情况。但是InheritableThreadLocal提供了一种父子线程之间的数据共享机制。可以解决这个问题。 staticThreadLocal<String>threadLocal=newThreadLocal<>();staticInheritableThreadLocal<String>inheritableThreadLocal=newInheritableThreadLocal<>();publicstaticvoidmain(String[]args)throwsInterruptedException{threadLocal.set("threadLocal主线程的值");Thread.sleep(100);newThread(()->System.out.println("子线程获取threadLocal的主线程值:"+threadLocal.get())).start();Thread.sleep(100);inheritableThreadLocal.set("inheritableThreadLocal主线程的值");newThread(()->System.out.println("子线程获取inheritableThreadLocal的主线程值:"+inheritableThreadLocal.get())).start();} 输出结果 线程获取threadLocal的主线程值:null子线程获取inheritableThreadLocal的主线程值:inheritableThreadLocal主线程的值 但是InheritableThreadLocal和线程池使用的时候就会存在问题,因为子线程只有在线程对象创建的时候才会把父线程inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中。这样就实现了父线程和子线程的上下文传递。但是线程池的话,线程会复用,所以会存在问题。如果要解决这个问题可以有什么办法列?大家可以思考下,或者在下方留言哦。如果实在不想思考的话,可以参考下阿里巴巴的transmittable-thread-local哦。 总结 大概介绍了 ThreadLocal的常见用法,以及大致实现原理,以及关于 ThreadLocal的内存泄露问题,以及关于使用它需要注意的事项,以及如何解决父子线程之间的传递。介绍了 ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local各种使用场景,以及需要注意的事项。本文重点介绍了 ThreadLocal,如果把这个弄清楚了,其他几种ThreadLocal就更好理解了。 结束 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。 感谢您的阅读,十分欢迎并感谢您的关注。 巨人的肩膀摘苹果: https://zhuanlan.zhihu.com/p/40515974 https://www.cnblogs.com/aspirant/p/8991010.html https://www.cnblogs.com/jiangxinlingdu/p/11123538.html https://blog.csdn.net/hewenbo111/article/details/80487252 最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构、等等。获取方式:点“在看”,关注公众号并回复 666领取,更多内容陆续奉上。文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*) 本文分享自微信公众号 - java金融(java4299)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册