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

AllAboutThreadLocal

时间:2023-04-02 10:29:59 Java

各位新老读者早上好,我是七夕(xī)。今天给大家分享一下采访常驻嘉宾:鹅厂ThreadLocal被问到。该问题的答案在下文的第2点中。1.底层结构ThreadLocal的底层结构由一个默认容量为16的数组组成,k是ThreadLocal对象的引用,v是要放入TheadLocal的值publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}ThreadLocalMap(ThreadLocalfirstKey,ObjectfirstValue){table=newEntry[INITIAL_CAPACITY];inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);table[i]=newEntry(firstKey,firstValue);大小=1;setThreshold(INITIAL_CAPACITY);}数组类似于一个HashMap,hash冲突不是用链表/红黑树来处理的,而是用链地址的方式,即尽量放在的下一个下标哈希冲突下标标记位置。阵列也可以扩展。2.工作原理一个ThreadLocal对象维护了一个ThreadLocalMap内部类对象,ThreadLocalMap对象是存放键值的地方。更准确的说,ThreadLocalMap的Entry内部类是存放键值的地方,见源码set()、createMap()。因为一个Thread对象维护了一个ThreadLocal.ThreadLocalMap成员变量,当ThreadLocal设置值时,得到的ThreadLocalMap就是当前线程对象的ThreadLocalMap。//获取ThreadLocalMap源码ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}这样每个线程都不会干扰ThreadLocal的操作,即ThreadLocal可以实现线程隔离3.使用ThreadLocalthreadLocal=newThreadLocal<>();threadLocal.set("七夕在学习Java");Integeri=threadLocal.get()//i=七夕在学习Java4.为什么ThreadLocal.ThreadLocalMap底层是长度为16的数组?对ThreadLocal的操作见第3点,可以看到ThreadLocal的每个set方法都操作同一个key(因为是同一个ThreadLocal对象,所以key必须相同)。这样一来,ThreadLocal上的操作好像永远只会存1个值,那用长度为1的数组不好吗?为什么使用16的长度?嗯,其实这里有一点需要注意。ThreadLocal可以存储多个值,那么如何存储多个值呢?看下面的代码://在主线程中执行下面的代码:ThreadLocalthreadLocal=newThreadLocal<>();threadLocal.set("七夕在学Java");ThreadLocalthreadLocal2=newThreadLocal<>();threadLocal2.set("七夕在学习Java2");执行代码后,看似新创建了2个ThreadLocal对象,但实际上数据存储是在同一个ThreadLocal.ThreadLocalMap上操作的。再次强调:ThreadLocal.ThreadLocalMap是访问数据的地方,ThreadLocal只是api调用的入口)。真相就在ThreadLocal类源码的getMap()中,所以上面代码最终的结果是一个ThreadLocalMap,里面存储了2个不同的ThreadLocal对象作为键,对应的值为七夕正在学习Java和七夕正在学习Java2。再看ThreadLocal的set方法publicvoidset(Tvalue){Threadt=Thread.currentThread();//这里,每次set之前,都会调用getMap(t)方法,t是当前调用set方法的线程ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}//重点:返回调用set方法的线程的ThreadLocal(本例为主线程)对象。//所以无论api调用者new了多少ThreadLocal对象,它总会返回调用线程(比如主线程)的ThreadLocal.ThreadLocalMap对象,供调用线程访问数据。ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}//t.threadLocals的声明如下ThreadLocal.ThreadLocalMapthreadLocals=null;//只有一个构造函数publicThreadLocal(){}5.数据存储在数组中,那么如何解决hash冲突问题,就是通过链地址法来解决的。如何解决?查看get和set方法何时执行:set:根据ThreadLocal对象的hash值定位ThreadLocalMap数组中的位置。如果该位置没有元素,则直接放在该位置。如果有元素且数组的key等于ThreadLocal,则覆盖该位置的元素;否则,寻找下一个空位,直到找到一个空位或者key相等。get:根据ThreadLocal对象的hash值定位到ThreadLocalMap数组中的位置。如果不是,则判断下一个位置;否则直接取出//数组元素结构Entry(ThreadLocalk,Objectv){super(k);value=v;}6.ThreadLocal内存泄漏隐患三个介词知识:ThreadLocal对象维护了一个ThreadLocalMap内部类ThreadLocalMap对象维护了一个Entry内部类,该类继承了弱引用WeakReference>,用于将ThreadLocal对象作为key进行存储(见最下方Entry构造方法源码),见最终源码部分。不管当前内存空间是否足够,JVM在GC时都会回收弱引用的内存,因为ThreadLocal被Entry中的Key变量作为弱引用引用,所以如果ThreadLocal没有外部强引用引用,那么ThreadLocal就会在下一次JVM垃圾回收中被回收。这时Entry中的key已经被回收了,但是value不会被垃圾回收器回收,因为它是强引用。这样,如果ThreadLocal线程继续运行,该值将不会被回收,从而导致内存泄漏。如果想避免内存泄露,可以使用ThreadLocal对象的remove()方法7.为什么ThreadLocalMap的key是弱引用staticclassThreadLocalMap{staticclassEntryextendsWeakReference>{/**与此ThreadLocal关联的值。*/对象值;条目(ThreadLocal<?>k,对象v){超级(k);值=v;为什么要这样设计,分两种情况讨论:key使用强引用:只有当创建ThreadLocal的线程还在运行时,那么ThreadLocalMap的key值才会泄漏内存,因为生命周期ThreadLocalMap与创建它的Thread对象相同。Key使用弱引用:这是一种拯救措施。至少弱引用的值可以及时被GC,减少内存泄漏。另外,即使不手动删除,作为key的ThreadLocal也会被回收。因为ThreadLocalMap在调用set、get、remove的时候,会先判断之前的值对应的key和当前调用的key是否相等。如果不相等,说明之前的key已经被回收了,此时value也会被回收。因此,对键使用弱引用是最优方案。8.(父子线程)如何共享ThreadLocal数据当主线程创建一个InheritableThreadLocal对象时,会为t.inheritableThreadLocals变量创建一个ThreadLocalMap并初始化。其中t为当前线程,即主线程创建子线程时,Thread的构造函数会检查其父线程的inheritableThreadLocals是否为null。从第一步可以知道不为null,然后将父线程的inheritableThreadLocals变量的值复制给子线程。InheritableThreadLocal重写getMap、createMap,使用Thread.inheritableThreadLocals变量如下:线程t,TfirstValue){t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);}}第二步:创建子线程时,判断父线程的inheritableThreadLocals是否为空。Non-nullcopy//在Thread构造方法中,会执行下面的逻辑第三步:使用对象作为第一步创建的inheritableThreadLocals对象}}例子://结果:可以输出“父线程-七夕在学习Java”ThreadLocalthreadLocal=newInheritableThreadLocal();threadLocal.set(“父线程-七夕在学习Java”);Threadt=newThread(()->System.out.println(threadLocal.get()));t.start();//结果:null,不能输出"子线程-七夕正在学习Java"ThreadLocalthreadLocal2=newInheritableThreadLocal();Threadt2=newThread(()->{threadLocal2.set("子线程-七夕正在学习Java");});t2.start();System.out.println(threadLocal2.get());第一篇公众号:七夕在学习Java,持续产出原创Java后端干货。如果对你有帮助,临走前能不能点个赞?