【高并发】ThreadLocal学会了这些,你也能和面试官扯皮了!
点击上方蓝色“冰河技术”,关注并选择“设为星标” 持之以恒,贵在坚持,每天进步一点点! 作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址: https://github.com/sunshinelyz/mykit-delay PS: 欢迎各位Star源码,也可以pr你牛逼哄哄的代码。 前言 我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。 对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量。 什么是ThreadLocal? ThreadLocal是JDK提供的,支持线程本地变量。也就是说,如果我们创建了一个ThreadLocal变量,则访问这个变量的每个线程都会有这个变量的一个本地副本。如果多个线程同时对这个变量进行读写操作时,实际上操作的是线程自己本地内存中的变量,从而避免了线程安全的问题。 ThreadLocal使用示例 例如,我们使用ThreadLocal保存并打印相关的变量信息,程序如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//创建第一个线程ThreadthreadA=newThread(()->{threadLocal.set("ThreadA:"+Thread.currentThread().getName());System.out.println("线程A本地变量中的值为:"+threadLocal.get());});//创建第二个线程ThreadthreadB=newThread(()->{threadLocal.set("ThreadB:"+Thread.currentThread().getName());System.out.println("线程B本地变量中的值为:"+threadLocal.get());});//启动线程A和线程BthreadA.start();threadB.start();}} 运行程序,打印的结果信息如下所示。 线程A本地变量中的值为:ThreadA:Thread-0线程B本地变量中的值为:ThreadB:Thread-1 此时,我们为线程A增加删除ThreadLocal中的变量的操作,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//创建第一个线程ThreadthreadA=newThread(()->{threadLocal.set("ThreadA:"+Thread.currentThread().getName());System.out.println("线程A本地变量中的值为:"+threadLocal.get());threadLocal.remove();System.out.println("线程A删除本地变量后ThreadLocal中的值为:"+threadLocal.get());});//创建第二个线程ThreadthreadB=newThread(()->{threadLocal.set("ThreadB:"+Thread.currentThread().getName());System.out.println("线程B本地变量中的值为:"+threadLocal.get());System.out.println("线程B没有删除本地变量:"+threadLocal.get());});//启动线程A和线程BthreadA.start();threadB.start();}} 此时的运行结果如下所示。 线程A本地变量中的值为:ThreadA:Thread-0线程B本地变量中的值为:ThreadB:Thread-1线程B没有删除本地变量:ThreadB:Thread-1线程A删除本地变量后ThreadLocal中的值为:null 通过上述程序我们可以看出,线程A和线程B存储在ThreadLocal中的变量互不干扰,线程A存储的变量只能由线程A访问,线程B存储的变量只能由线程B访问。 ThreadLocal原理 首先,我们看下Thread类的源码,如下所示。 publicclassThreadimplementsRunnable{/***********省略N行代码*************/ThreadLocal.ThreadLocalMapthreadLocals=null;ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;/***********省略N行代码*************/} 由Thread类的源码可以看出,在ThreadLocal类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。 这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程的threadLocals变量里面的。也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。 接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。 set()方法 set()方法的源代码如下所示。 publicvoidset(Tvalue){//获取当前线程Threadt=Thread.currentThread();//以当前线程为Key,获取ThreadLocalMap对象ThreadLocalMapmap=getMap(t);//获取的ThreadLocalMap对象不为空if(map!=null)//设置value的值map.set(this,value);else//获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量createMap(t,value);} 在set()方法中,首先获取调用set()方法的线程,接下来,使用当前线程作为Key调用getMap(t)方法来获取ThreadLocalMap对象,getMap(Thread t)的方法源码如下所示。 ThreadLocalMapgetMap(Threadt){returnt.threadLocals;} 可以看到,getMap(Thread t)方法获取的是线程变量自身的threadLocals成员变量。 在set()方法中,如果调用getMap(t)方法返回的对象不为空,则把value值设置到Thread类的threadLocals成员变量中,而传递的key为当前ThreadLocal的this对象,value就是通过set()方法传递的值。 如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。 voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);} 也就是创建当前线程的threadLocals变量。 get()方法 get()方法的源代码如下所示。 publicTget(){//获取当前线程Threadt=Thread.currentThread();//获取当前线程的threadLocals成员变量ThreadLocalMapmap=getMap(t);//获取的threadLocals变量不为空if(map!=null){//返回本地变量对应的值ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}//初始化threadLocals成员变量的值returnsetInitialValue();} 通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。 privateTsetInitialValue(){//调用初始化Value的方法Tvalue=initialValue();Threadt=Thread.currentThread();//根据当前线程获取threadLocals成员变量ThreadLocalMapmap=getMap(t);if(map!=null)//threadLocals不为空,则设置value值map.set(this,value);else//threadLocals为空,创建threadLocals变量createMap(t,value);returnvalue;} 其中,initialValue()方法的源码如下所示。 protectedTinitialValue(){returnnull;} 通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。 remove()方法 remove()方法的源代码如下所示。 publicvoidremove(){//根据当前线程获取threadLocals成员变量ThreadLocalMapm=getMap(Thread.currentThread());if(m!=null)//threadLocals成员变量不为空,则移除value值m.remove(this);} remove()方法的实现比较简单,首先根据当前线程获取threadLocals成员变量,不为空,则直接移除value的值。 注意:如果调用线程一致不终止,则本地变量会一直存放在调用线程的threadLocals成员变量中,所以,如果不需要使用本地变量时,可以通过调用ThreadLocal的remove()方法,将本地变量从当前线程的threadLocals成员变量中删除,以免出现内存溢出的问题。 ThreadLocal变量不具有传递性 使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。 接下来,我们来看一段代码,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newThreadLocal<String>();publicstaticvoidmain(String[]args){//在主线程中设置值threadLocal.set("ThreadLocalTest");//在子线程中获取值Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("子线程获取值:"+threadLocal.get());}});//启动子线程thread.start();//在主线程中获取值System.out.println("主线程获取值:"+threadLocal.get());}} 运行这段代码输出的结果信息如下所示。 主线程获取值:ThreadLocalTest子线程获取值:null 通过上述程序,我们可以看出在主线程中向ThreadLocal设置值后,在子线程中是无法获取到这个值的。那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。 InheritableThreadLocal使用示例 InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值,例如,我们将ThreadLocalTest类中的threadLocal静态变量改写成InheritableThreadLocal类的实例,如下所示。 publicclassThreadLocalTest{privatestaticThreadLocal<String>threadLocal=newInheritableThreadLocal<String>();publicstaticvoidmain(String[]args){//在主线程中设置值threadLocal.set("ThreadLocalTest");//在子线程中获取值Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("子线程获取值:"+threadLocal.get());}});//启动子线程thread.start();//在主线程中获取值System.out.println("主线程获取值:"+threadLocal.get());}} 此时,运行程序输出的结果信息如下所示。 主线程获取值:ThreadLocalTest子线程获取值:ThreadLocalTest 可以看到,使用InheritableThreadLocal类存储本地变量时,子线程能够获取到父线程中设置的本地变量。 InheritableThreadLocal原理 首先,我们来看下InheritableThreadLocal类的源码,如下所示。 publicclassInheritableThreadLocal<T>extendsThreadLocal<T>{protectedTchildValue(TparentValue){returnparentValue;}ThreadLocalMapgetMap(Threadt){returnt.inheritableThreadLocals;}voidcreateMap(Threadt,TfirstValue){t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);}} 由InheritableThreadLocal类的源代码可知,InheritableThreadLocal类继承自ThreadLocal类,并且重写了ThreadLocal类的childValue()方法、getMap()方法和createMap()方法。也就是说,当调用ThreadLocal的set()方法时,创建的是当前Thread线程的inheritableThreadLocals成员变量而不再是threadLocals成员变量。 这里,我们需要思考一个问题:InheritableThreadLocal类的childValue()方法是何时被调用的呢?这就需要我们来看下Thread类的构造方法了,如下所示。 publicThread(){init(null,null,"Thread-"+nextThreadNum(),0);}publicThread(Runnabletarget){init(null,target,"Thread-"+nextThreadNum(),0);}Thread(Runnabletarget,AccessControlContextacc){init(null,target,"Thread-"+nextThreadNum(),0,acc,false);}publicThread(ThreadGroupgroup,Runnabletarget){init(group,target,"Thread-"+nextThreadNum(),0);}publicThread(Stringname){init(null,null,name,0);}publicThread(ThreadGroupgroup,Stringname){init(group,null,name,0);}publicThread(Runnabletarget,Stringname){init(null,target,name,0);}publicThread(ThreadGroupgroup,Runnabletarget,Stringname){init(group,target,name,0);}publicThread(ThreadGroupgroup,Runnabletarget,Stringname,longstackSize){init(group,target,name,stackSize);} 可以看到,Thread类的构造方法最终调用的是init()方法,那我们就来看下init()方法,如下所示。 privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){/************省略部分源码************/if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/*StashthespecifiedstacksizeincasetheVMcares*/this.stackSize=stackSize;/*SetthreadID*/tid=nextThreadID();} 可以看到,在init()方法中会判断传递的inheritThreadLocals变量是否为true,同时父线程中的inheritableThreadLocals是否为null,如果传递的inheritThreadLocals变量为true,同时,父线程中的inheritableThreadLocals不为null,则调用ThreadLocal类的createInheritedMap()方法。 staticThreadLocalMapcreateInheritedMap(ThreadLocalMapparentMap){returnnewThreadLocalMap(parentMap);} 在createInheritedMap()中,使用父线程的inheritableThreadLocals变量作为参数创建新的ThreadLocalMap对象。然后在Thread类的init()方法中会将这个ThreadLocalMap对象赋值给子线程的inheritableThreadLocals成员变量。 接下来,我们来看看ThreadLocalMap的构造函数都干了啥,如下所示。 privateThreadLocalMap(ThreadLocalMapparentMap){Entry[]parentTable=parentMap.table;intlen=parentTable.length;setThreshold(len);table=newEntry[len];for(intj=0;j<len;j++){Entrye=parentTable[j];if(e!=null){@SuppressWarnings("unchecked")ThreadLocal<Object>key=(ThreadLocal<Object>)e.get();if(key!=null){//调用重写的childValue方法Objectvalue=key.childValue(e.value);Entryc=newEntry(key,value);inth=key.threadLocalHashCode&(len-1);while(table[h]!=null)h=nextIndex(h,len);table[h]=c;size++;}}}} 在ThreadLocalMap的构造函数中,调用了InheritableThreadLocal类重写的childValue()方法。而InheritableThreadLocal类通过重写getMap()方法和createMap()方法,让本地变量保存到了Thread线程的inheritableThreadLocals变量中,线程通过InheritableThreadLocal类的set()方法和get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。此时,如果父线程创建子线程,在Thread类的构造函数中会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量中。 如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。 最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。 后记: 记住:你比别人强的地方,不是你做过多少年的CRUD工作,而是你比别人掌握了更多深入的技能。不要总停留在CRUD的表面工作,理解并掌握底层原理并熟悉源码实现,并形成自己的抽象思维能力,做到灵活运用,才是你突破瓶颈,脱颖而出的重要方向! 你在刷抖音,玩游戏的时候,别人都在这里学习,成长,提升,人与人最大的差距其实就是思维。你可能不信,优秀的人,总是在一起。。 扫一扫或长按二维码 关注冰河技术微信公众号 如果你喜欢这篇文章,欢迎点赞和转发。 生活很美好,明天见(。・ω・。)ノ♡ 本文分享自微信公众号 - 冰河技术(hacker-binghe)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。