当前位置: 首页 > 后端技术 > Java

ThreadLocal

时间:2023-04-01 13:54:03 Java

作为java的基础,早在JDK1.2版本就提供了java.lang.ThreadLocal。ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以非常简洁的写出漂亮的多线程程序。ThreadLocal指的是一个局部变量,作用域为Thread。或许把它命名为ThreadLocalVariable更容易理解。参考了本博客的很多内容(本博客https://www.cnblogs.com/fsmly...)。介绍了当多个线程访问同一个共享变量时容易出现并发问题,尤其是当多个线程访问一个变量时,为了保证线程安全,一般用户在访问共享变量时需要进行额外的同步措施,以保证线程安全。ThreadLocal是一种除了同步加锁的方法之外保证多线程访问中线程不安全的一种方法。当我们创建一个变量时,如果每个线程都访问它,那么访问的是线程自己的变量,这样就不会出现线程不安全的情况。ThreadLocal是JDK包提供的,它提供了线程局部变量。如果创建一个ThreadLocal变量,每个访问这个变量的线程都会有一个这个变量的副本。在实际的多线程操作中,操作是在自己的本地内存中进行的。变量,从而避免了线程安全问题,如下图所示:ThreadLocal使用示例下面的例子中,启动了两个线程,在每个线程内部设置局部变量的值,然后调用print方法打印当前局部变量的值。如果打印后调用了局部变量的remove方法,会删除本地内存中的变量,代码如下:packagetest;公共类ThreadLocalTest{staticThreadLocallocalVar=newThreadLocal<>();staticvoidprint(Stringstr){//在当前线程中打印本地内存中局部变量的值System.out.println(str+":"+localVar.get());//清除本地内存中的局部变量localVar.remove();}publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//设置线程1中局部变量的值localVar.set("localVar1");//调用打印方法print("thread1");//打印局部变量System.out.println("afterremove:"+localVar.get());}});Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){//设置线程1中局部变量的值localVar.set("localVar2");//调用打印方法print("thread2");//打印局部变量System.out.println("afterremove:"+localVar.get());}});t1.开始();t2.开始();}}ThreadLocal的实现原理从上一节我们可以看出,ThreadLocal主要有set和get方法,用于在线程中设置和获取变量,那么ThreadLocal是如何实现这个功能的呢?与ThreadLocal的实现相关的主要有三个类:ThreadLocal、Thread和ThreadLocalMap。三者的关系也如下图所示:ThreadLocalMap:名字看起来像Map,但实际上是一个数组,但作用与Map类似。可以根据关键字Thread查找资料:线程大家应该都知道,那么ThreadLocal中到底有什么作用呢?一个Thread会包含两个ThreadLocalMap,分别用来存放本线程和父线程的ThreadLocal数据。每个ThreadLocal变量都会在线程中对应一个ThreadLocalMapkey-value,其中key是ThreadLocal唯一的Hash值。ThreadLocal:每个ThreadLocal都会有一个唯一的Hash值,用于在ThreadLocalMap中查找这个ThreadLocal的值;ThreadLocal提供了获取当前线程的ThreadLocal数据的方法。数据存储的位置ThreadLocal只是一个访问线程数据的外壳。ThreadLocalget和set的数据不会存储在ThreadLocal的实例中,而是存储在线程Thread中的ThreadLocalMap中。ThreadLocal只是提供了一种访问这些数据的方法。ThreadLoca的set方法为调用线程的ThreadLocalMap增加值,当调用线程调用get方法时,可以从它的ThreadLocalMap中取出变量。如果调用线程永不终止,那么这个局部变量会一直保存在它的ThreadLocalMap中,所以当这个局部变量不用的时候,需要调用remove方法把不用的局部变量从ThreadLocalMap中删除。set方法存储数据。ThreadLocal方法的set方法可以将数据放入当前线程的ThreadLocalMap中。存储数据的源码如下。Set过程分为以下几个步骤:获取当前线程。从当前线程获取ThreadLocalMap变量。如果当前线程的ThreadLocalMap不为空,则使用当前ThreadLocal作为Key,将要存储的数据作为Value来存储数据。如果当前线程的ThreadLocalMap为空,则创建一个ThreadLocalMap并存储数据。publicvoidset(Tvalue){//(1)获取当前线程(调用线程)Threadt=Thread.currentThread();//(2)以当前线程为键值,查找对应的线程变量,找到对应的mapThreadLocalMapmap=getMap(t);//(3)如果map不为null,直接添加局部变量,key为当前定义的ThreadLocal变量的this引用,value为添加的局部变量值if(map!=null)map.set(这个,价值);//(4)如果map为null,说明是第一次添加,需要先创建对应的mapelsecreateMap(t,value);}get方法获取数据ThreadLocal方法get即可获取当前线程ThreadLocalMap中存储的数据。获取存储数据的源码如下。get过程分为以下几步:获取当前线程,从当前线程获取ThreadLocalMap变量。如果ThreadLocalMap变量不为null,则可以在map中找到局部变量的值。如果ThreadLocalMap变量为null,则初始化当前线程的ThreadLocalMap。publicTget(){//(1)获取当前线程Threadt=Thread.currentThread();//(2)获取当前线程的threadLocals变量ThreadLocalMapmap=getMap(t);//(3)如果threadLocals变量不为null,则可以在map中找到局部变量的值if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")T结果=(T)e.value;返回结果;}}//(4)这里执行,threadLocals为null,调用这个change来初始化当前线程的threadLocals变量returnsetInitialValue();}privateTsetInitialValue(){//protectedTinitialValue(){returnnull;}T值=初始值();//获取当前线程Threadt=Thread.currentThread();//以当前线程为键值查找对应的线程变量,找到对应的mapThreadLocalMapmap=getMap(t);//如果map不为null,直接添加局部变量,key为当前线程,value为添加的局部变量值if(map!=null)map.set(this,value);//如果map为null,表示是第一次添加,需要先创建对应的mapelsecreateMap(t,value);returnvalue;}ThreadLocal不支持在父线程中设置同一个ThreadLocal变量后继承,在子线程中无法获取。(ThreadLocals是当前调用线程对应的局部变量,所以两者自然不能共享)。packagetest;publicclassThreadLocalTest2{//(1)创建ThreadLocal变量publicstaticThreadLocalthreadLocal=newThreadLocal<>();publicstaticvoidmain(String[]args){//添加主线程到主线程局部变量threadLocal.set("mainVal");//新建一个子线程Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("localinthesub-threadVariablevalue:"+threadLocal.get());}});thread.start();//输出主线程中的局部变量值System.out.println("主线程中的局部变量值:"+threadLocal.get());}}InheritableThreadLocal类上面提到的ThreadLocal类不能提供子线程访问父线程的局部变量,但是InheritableThreadLocal类可以做到这个功能。下面是这个类的源代码。InheritableThreadLocal类继承了ThreadLocal类,重写了childValue、getMap、createMap三个方法。下面分别介绍这三种方法的用处。createMap:当线程中不存在ThreadLocalMap变量,但调用set或get方法设置值时,需要初始化ThreadLocalMap变量时调用该方法。getMap:当需要获取线程的ThreadLocalMap时调用该方法。这里返回的ThreadLocalMap始终是InheritableThreadLocalMap。childValue:创建新线程时,如果父线程有ThreadLocalMap变量,允许继承ThreadLocalMap,那么程序会将父线程的InheritableThreadLocal复制给子线程,childValue表示如何根据数据复制过程中从父线程获取的数据。publicclassInheritableThreadLocalextendsThreadLocal{/***创建一个可继承的线程局部变量。*/publicInheritableThreadLocal(){}/***计算此可继承线程局部*变量的子项初始值,作为创建子*线程时父项值的函数。在子进程启动之前,从父线程中调用此方法。*

*此方法仅返回其输入参数,如果需要不同的行为,则应覆盖*。*/protectedTchildValue(TparentValue){returnparentValue;}/***获取与ThreadLocal关联的地图。*/ThreadLocalMapgetMap(Threadt){returnt.inheritableThreadLocals;}/***创建与ThreadLocal关联的映射。*/voidcreateMap(Threadt,TfirstValue){t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);}}总结:Thread会在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值给新建的ThreadLocalMap对象,返回给子线程的inheritableThreadLocals。InheritableThreadLocals类通过重写getMap和createMap这两个方法,将局部变量保存在特定线程的inheritableThreadLocals变量中。当线程通过InheritableThreadLocals实例的set或get方法设置变量时,会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制到子线程的inheritableThreadLocals变量中。ThreadLocal内存泄漏通过前面的分析我们知道ThreadLocal的线程数据是存放在ThreadLocalMap中的,所以如果ThreadLocal内存泄漏,那么ThreadLocalMap中存放的数据肯定是泄漏了。我们需要看一下ThreadLocalMap中的数据结构。ThreadLocalMap的数据结构如下。ThreadLocalMap中的数据存储在一个Entry数组中,Entry中有一个ThreadLocal的WeakReference。什么情况下会发生内存泄漏?当一个线程调用ThreadLocal的set方法设置一个变量时,当前线程的ThreadLocalMap会存储一条记录。这条记录的键值为ThreadLocal的弱引用,值为set设置的值。如果当前线程一直存在,没有调用ThreadLocal的remove方法,如果此时其他地方有ThreadLocal的引用,那么当前线程中的ThreadLocalMap中就会有ThreadLocal变量的引用和ThreadLocalMap中值对象的引用.释放后,会造成内存泄漏。考虑到这个ThreadLocal变量没有其他强依赖性。如果当前线程还存在,由于线程的ThreadLocalMap中的key是弱引用,gc时会回收当前线程的ThreadLocalMap中的ThreadLocal变量的弱引用,但是对应的value还是存在的,这可能会造成内存泄漏(因为此时ThreadLocalMap会有一个key为null但value不为null的入口项)。总结:THreadLocalMap中的entrykey使用了ThreadLocal对象的弱引用。如果没有其他地方依赖ThreadLocal,ThreadLocalMap中的ThreadLocal对象会被回收,但是对应的不会被回收。这时,可能在Map中存在key为null但value不为null的项,这就需要在使用后及时调用remove方法,避免内存泄漏。Java中的四种引用类型上面我们提到了WeakReference。你可能对这个词有点陌生。Java中有四种引用类型:强引用:Java中默认的引用类型。如果一个对象存在强引用,那么只要这个引用还存在,就不会被GC。软引用:简而言之,如果一个对象存在弱引用,那么在JVMOOM发生之前(即内存被完全使用),该对象不会被GC;只有当JVM内存不足时,该对象才会被GC。软引用与引用队列结合使用。如果软引用所引用的对象被回收,则该引用将被添加到与其关联的引用队列中。弱引用(这里是ThreadLocalMap中的Entry类的重点):如果一个对象只有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用引用的对象只能存活到下一次GC,当GC发生时,不管当前内存是否充足,弱引用引用的对象都会被回收)。弱引用也与引用队列结合使用。如果弱引用对象被垃圾回收期回收了,JVM会将这个引用添加到与其关联的引用队列中。如果可以通过弱引用的get方法获取引用对象,则引用对象被回收后,调用get方法会返回null;幻影引用:幻影引用是所有引用中最弱的一种引用,它的存在是为了在与幻影引用关联的对象被GC后收到通知。(它指向的对象不能通过get方法获取)。我是狐神,欢迎大家关注我的微信公众号:wzm2zsd本文首发于微信公众号,版权所有,禁止转载!