Java并发编程的艺术ThreadLocal原理和使用
带着BAT大厂的面试问题去理解
请带着这些问题继续后文,会很大程度上帮助你更好地理解相关知识点。什么是ThreadLocal?用来解决什么问题的?说说你对ThreadLocal的理解ThreadLocal是如何实现线程隔离的?为什么ThreadLocal会造成内存泄露?如何解决还有哪些使用ThreadLocal的应用场景?ThreadLocal简介
我们在Java并发并发理论基础总结过线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:互斥同步:synchronized和ReentrantLock非阻塞同步:CAS,AtomicXXXX无同步方案:栈封闭,本地存储(ThreadLocal),可重入代码
这个章节将详细地讲讲本地存储(ThreadLocal)。官网的解释是这样的:
Thisclassprovidesthreadlocalvariables。Thesevariablesdifferfromtheirnormalcounterpartsinthateachthreadthataccessesone(viaits{codeget}or{codeset}method)hasitsown,independentlyinitializedcopyofthevariable。{codeThreadLocal}instancesaretypicallyprivatestaticfieldsinclassesthatwishtoassociatestatewithathread(e。g。,auserIDorTransactionID)该类提供了线程局部(threadlocal)变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get或set方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal实例通常是类中的privatestatic字段,它们希望将状态与某一个线程(例如,用户ID或事务ID)相关联。
总结而言:ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类;当使用ThreadLocal来维护变量时,ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。ThreadLocal理解
提到ThreadLocal被提到应用最多的是session管理和数据库链接管理,这里以数据访问为例帮助你理解ThreadLocal:如下数据库管理类在单线程使用是没有任何问题的classConnectionManager{privatestaticConnectionconnectnull;publicstaticConnectionopenConnection(){if(connectnull){connectDriverManager。getConnection();}returnconnect;}publicstaticvoidcloseConnection(){if(connect!null)connect。close();}}
很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。为了解决上述线程安全的问题,第一考虑:互斥同步
你可能会说,将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。这里再抛出一个问题:这地方到底需不需要将connect变量进行共享?
事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。即改后的代码可以这样:classConnectionManager{privateConnectionconnectnull;publicConnectionopenConnection(){if(connectnull){connectDriverManager。getConnection();}returnconnect;}publicvoidcloseConnection(){if(connect!null)connect。close();}}classDao{publicvoidinsert(){ConnectionManagerconnectionManagernewConnectionManager();ConnectionconnectionconnectionManager。openConnection();使用connection进行操作connectionManager。closeConnection();}}
确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。这时候ThreadLocal登场了
那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:importjava。sql。Connection;importjava。sql。DriverManager;importjava。sql。SQLException;publicclassConnectionManager{privatestaticfinalThreadLocalConnectiondbConnectionLocalnewThreadLocalConnection(){OverrideprotectedConnectioninitialValue(){try{returnDriverManager。getConnection(,,);}catch(SQLExceptione){e。printStackTrace();}returnnull;}};publicConnectiongetConnection(){returndbConnectionLocal。get();}}再注意下ThreadLocal的修饰符
ThreaLocal的JDK文档中说明:ThreadLocalinstancesaretypicallyprivatestaticfieldsinclassesthatwishtoassociatestatewithathread。如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义privatestatic类型的ThreadLocal实例。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。ThreadLocal原理如何实现线程隔离
主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals,负责存储当前线程的关于Connection的对象,dbConnectionLocal(以上述例子中为例)这个变量为Key,以新建的Connection对象为Value;这样的话,线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个Connection对象并且返回;
具体关于为线程分配变量副本的代码如下:publicTget(){ThreadtThread。currentThread();ThreadLocalMapthreadLocalsgetMap(t);if(threadLocals!null){ThreadLocalMap。EntryethreadLocals。getEntry(this);if(e!null){SuppressWarnings(unchecked)Tresult(T)e。value;returnresult;}}returnsetInitialValue();}首先获取当前线程对象t,然后从线程t中获取到ThreadLocalMap的成员属性threadLocals如果当前线程的threadLocals已经初始化(即不为null)并且存在以当前ThreadLocal对象为Key的值,则直接返回当前线程要获取的对象(本例中为Connection);如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象,那么重新创建一个Connection对象,并且添加到当前线程的threadLocalsMap中,并返回如果当前线程的threadLocals属性还没有被初始化,则重新创建一个ThreadLocalMap对象,并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。
如果存在则直接返回很好理解,那么对于如何初始化的代码又是怎样的呢?privateTsetInitialValue(){TvalueinitialValue();ThreadtThread。currentThread();ThreadLocalMapmapgetMap(t);if(map!null)map。set(this,value);elsecreateMap(t,value);returnvalue;}首先调用我们上面写的重载过后的initialValue方法,产生一个Connection对象继续查看当前线程的threadLocals是不是空的,如果ThreadLocalMap已被初始化,那么直接将产生的对象添加到ThreadLocalMap中,如果没有初始化,则创建并添加对象到其中;
同时,ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法publicvoidset(Tvalue){ThreadtThread。currentThread();ThreadLocalMapmapgetMap(t);if(map!null)map。set(this,value);elsecreateMap(t,value);}
这样我们也可以不实现initialValue,将初始化工作放到DBConnectionFactory的getConnection方法中:publicConnectiongetConnection(){ConnectionconnectiondbConnectionLocal。get();if(connectionnull){try{connectionDriverManager。getConnection(,,);dbConnectionLocal。set(connection);}catch(SQLExceptione){e。printStackTrace();}}returnconnection;}
那么我们看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了;其实就是用了Map的数据结构给当前线程缓存了,要使用的时候就从本线程的threadLocals对象中获取就可以了,key就是当前线程;
当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了,当然能做到变量的线程间隔离了;
现在我们知道了ThreadLocal到底是什么了,又知道了如何使用ThreadLocal以及其基本实现原理了是不是就可以结束了呢?其实还有一个问题就是ThreadLocalMap是个什么对象,为什么要用这个对象呢?ThreadLocalMap对象是什么
本质上来讲,它就是一个Map,但是这个ThreadLocalMap与我们平时见到的Map有点不一样它没有实现Map接口;它没有public的方法,最多有一个default的构造方法,因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用,属于静态内部类ThreadLocalMap的Entry实现继承了WeakReferenceThreadLocallt;?该方法仅仅用了一个Entry数组来存储Key,Value;Entry并不是链表形式,而是每个bucket里面仅仅放一个Entry;
要了解ThreadLocalMap的实现,我们先从入口开始,就是往该Map中添加一个值:privatevoidset(ThreadLocallt;?key,Objectvalue){Wedontuseafastpathaswithget()becauseitisatleastascommontouseset()tocreatenewentriesasitistoreplaceexistingones,inwhichcase,afastpathwouldfailmoreoftenthannot。Entry〔〕tabtable;intlentab。length;intikey。threadLocalHashCode(len1);for(Entryetab〔i〕;e!null;etab〔inextIndex(i,len)〕){ThreadLocallt;?ke。get();if(kkey){e。valuevalue;return;}if(knull){replaceStaleEntry(key,value,i);return;}}tab〔i〕newEntry(key,value);intszsize;if(!cleanSomeSlots(i,sz)szthreshold)rehash();}
先进行简单的分析,对该代码表层意思进行解读:看下当前threadLocal的在数组中的索引位置比如:i2,看i2位置上面的元素(Entry)的Key是否等于threadLocal这个Key,如果等于就很好说了,直接将该位置上面的Entry的Value替换成最新的就可以了;如果当前位置上面的Entry的Key为空,说明ThreadLocal对象已经被回收了,那么就调用replaceStaleEntry如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小阈值的时候对当前的Table进行重新哈希所以,该HashMap是处理冲突检测的机制是向后移位,清除过期条目最终找到合适的位置;
了解完Set方法,后面就是Get方法了:privateEntrygetEntry(ThreadLocallt;?key){intikey。threadLocalHashCode(table。length1);Entryetable〔i〕;if(e!nulle。get()key)returne;elsereturngetEntryAfterMiss(key,i,e);}
先找到ThreadLocal的索引位置,如果索引位置处的entry不为空并且键与threadLocal是同一个对象,则直接返回;否则去后面的索引位置继续查找。ThreadLocal造成内存泄露的问题
网上有这样一个例子:importjava。util。concurrent。LinkedBlockingQueue;importjava。util。concurrent。ThreadPoolExecutor;importjava。util。concurrent。TimeUnit;publicclassThreadLocalDemo{staticclassLocalVariable{privateLong〔〕anewLong〔10241024〕;}(1)finalstaticThreadPoolExecutorpoolExecutornewThreadPoolExecutor(5,5,1,TimeUnit。MINUTES,newLinkedBlockingQueue());(2)finalstaticThreadLocalLocalVariablelocalVariablenewThreadLocalLocalVariable();publicstaticvoidmain(String〔〕args)throwsInterruptedException{(3)Thread。sleep(50004);for(inti0;i50;i){poolExecutor。execute(newRunnable(){publicvoidrun(){(4)localVariable。set(newLocalVariable());(5)System。out。println(uselocalvaraiblelocalVariable。get());localVariable。remove();}});}(6)System。out。println(poolexecuteover);}}
如果用线程池来操作ThreadLocal对象确实会造成内存泄露,因为对于线程池里面不会销毁的线程,里面总会存在着ThreadLocal,LocalVariable的强引用,因为finalstatic修饰的ThreadLocal并不会释放,而ThreadLocalMap对于Key虽然是弱引用,但是强引用不会释放,弱引用当然也会一直有值,同时创建的LocalVariable对象也不会释放,就造成了内存泄露;如果LocalVariable对象不是一个大对象的话,其实泄露的并不严重,泄露的内存核心线程数LocalVariable对象的大小;
所以,为了避免出现内存泄露的情况,ThreadLocal提供了一个清除线程中对象的方法,即remove,其实内部实现就是调用ThreadLocalMap的remove方法:privatevoidremove(ThreadLocallt;?key){Entry〔〕tabtable;intlentab。length;intikey。threadLocalHashCode(len1);for(Entryetab〔i〕;e!null;etab〔inextIndex(i,len)〕){if(e。get()key){e。clear();expungeStaleEntry(i);return;}}}
找到Key对应的Entry,并且清除Entry的Key(ThreadLocal)置空,随后清除过期的Entry即可避免内存泄露。再看ThreadLocal应用场景
除了上述的数据库管理类的例子,我们再看看其它一些应用:每个线程维护了一个序列号
再回想上文说的,如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义privatestatic类型的ThreadLocal实例。
每个线程维护了一个序列号publicclassSerialNum{ThenextserialnumbertobeassignedprivatestaticintnextSerialNum0;privatestaticThreadLocalserialNumnewThreadLocal(){protectedsynchronizedObjectinitialValue(){returnnewInteger(nextSerialNum);}};publicstaticintget(){return((Integer)(serialNum。get()))。intValue();}}Session的管理
经典的另外一个例子:privatestaticfinalThreadLocalthreadSessionnewThreadLocal();publicstaticSessiongetSession()throwsInfrastructureException{Sessions(Session)threadSession。get();try{if(snull){sgetSessionFactory()。openSession();threadSession。set(s);}}catch(HibernateExceptionex){thrownewInfrastructureException(ex);}returns;}在线程内部创建ThreadLocal
还有一种用法是在线程类内部创建ThreadLocal,基本步骤如下:在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。publicclassThreadLocalTestimplementsRunnable{ThreadLocalStudentStudentThreadLocalnewThreadLocalStudent();Overridepublicvoidrun(){StringcurrentThreadNameThread。currentThread()。getName();System。out。println(currentThreadNameisrunning。。。);RandomrandomnewRandom();intagerandom。nextInt(100);System。out。println(currentThreadNameissetage:age);StudentStudentgetStudentt();通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值Student。setAge(age);System。out。println(currentThreadNameisfirstgetage:Student。getAge());try{Thread。sleep(500);}catch(InterruptedExceptione){e。printStackTrace();}System。out。println(currentThreadNameissecondgetage:Student。getAge());}privateStudentgetStudentt(){StudentStudentStudentThreadLocal。get();if(nullStudent){StudentnewStudent();StudentThreadLocal。set(Student);}returnStudent;}publicstaticvoidmain(String〔〕args){ThreadLocalTesttnewThreadLocalTest();Threadt1newThread(t,ThreadA);Threadt2newThread(t,ThreadB);t1。start();t2。start();}}classStudent{intage;publicintgetAge(){returnage;}publicvoidsetAge(intage){this。ageage;}}java开发手册中推荐的ThreadLocal
看看阿里巴巴java开发手册中推荐的ThreadLocal的用法:importjava。text。DateFormat;importjava。text。SimpleDateFormat;publicclassDateUtils{publicstaticfinalThreadLocalDateFormatdfnewThreadLocalDateFormat(){OverrideprotectedDateFormatinitialValue(){returnnewSimpleDateFormat(yyyyMMdd);}};}
然后我们再要用到DateFormat对象的地方,这样调用:DateUtils。df。get()。format(newDate());
高端局战士永远只有这4个,若都是本命,你必是王者王者荣耀对抗路是战士们博弈最为厉害的一个位置,预判意识、吹毛求疵的操作缺一不可,如果拒绝开麦和队友沟通,那么就需要时刻警惕中路、打野的动向,对抗路玩家心思需要十分细腻,否则很容……
全球最快相机诞生每秒可捕捉到70万亿帧据外媒报道,最好的手机摄像头可以实现每秒记录下1000帧以下的慢动作。然而跟全球最快的摄像机的新纪录保持者每秒70万亿帧的惊人速度相比,这一切都显得黯然失色。这个速度足以捕捉运……
索尼展示原型VR控制器可实时追踪用户的手和手指索尼互动娱乐公司今天官方分享了一段视频,展示了一款可以追踪整个人类手部的原型控制器。这款控制器能够克服目前手势控制系统和传统控制器存在的诸多限制,能够在不破坏虚拟现实体验的情况……
惠普OMEN27i游戏显示器IPS面板售价499。99美元伴随着新款OMEN游戏台式机,惠普今天还推出了OMEN27i显示器。相比较惠普此前多款游戏屏幕所常用的的TN面板,这款QHD(25601440)分辨率的27英寸的显示器采用了n……
你可以遛机器狗了宇树科技推出四足机器狗A1在不远的未来你可以遛机器狗了。在IEEE的最新报道中,介绍了来自杭州宇树科技(Unitree)最新研发的四足机器狗A1,售价不超过10000美元。宇树科技被认为是中国的波士顿动……
开心果的健康益处如此之好,简直是太值得了另外开心果的营养成分会让你立刻打开一把。下次你想吃松脆的零食时,可以考虑吃开心果。它们带有令人愉悦的泥土味、奶油味和甜味,让它们吃起来超级满足。但除了令人垂涎的味道和质地……
商标申请曝光三星新款真无线耳机叫做BudsX三星在今年早些时候发布了GalaxyS20系列智能机和GalaxyBuds真无线耳机,得益于双路动态扬声器的改进,耳机可带来比以往更丰富的音频体验。与初代GalaxyBuds相……
谷歌开售新款PixelBuds真无线耳机纯白版179美元谷歌近日终于放开了新款PixelBuds真无线耳机的销售,在修复初代产品缺陷的同时,还有望带领Android阵营向苹果AirPods发起更强势的进攻。去年10月,谷歌随Pixe……
三星新款真无线耳机或被命名为GalaxyBudsX早前有传闻称,三星即将推出名为GalaxyBeans的真无线耳机。然而知名爆料人MaxJ。在Twitter上透露,这款产品的正式名称,或叫做GalaxyBudsX。据说该耳机的……
圆融之人,其禄必厚执拗者福轻,而圆融之人,其禄必厚;操切者寿夭,而宽厚之士其年必长;故君子不言命,养性即所以立命;亦不言天,尽人自可以回天。性格固执的人福分微薄,而性格灵活通融的人福气大;……
AppleWatch5岁了!设计草图首次曝光ImranChaudhri在苹果工作了20多年,帮助创造了iPhone、iPad和AppleWatch等知名产品。今天是初代AppleWatch发布五周年,Chaudhri分享……
当贝发布智能投影当贝F3可学习用户观看习惯昨天,当贝公司发布投影新品当贝投影F3以及当贝超级盒子H1。其中,当贝投影F3拥有2050ANSI流明亮度,内置Mstar938芯片和三星原装4GB64GBeMMC5。1……