本文转载自微信公众号“小菜两记”,作者小菜两记。转载本文请联系小菜良记公众号。ThreadLocal概念介绍ThreadLocal类用于提供线程内的局部变量。这种变量在多线程环境下访问时(get和set方法访问),可以保证每个线程的变量相对独立于其他线程中的变量。ThreadLocal实例一般是私有静态类型,用于关联线程和上下文。FunctionTransfer数据提供线程内部的局部变量。通过ThreadLocal可以在同一个线程和不同的组件中传递公共变量。线程并发适用于多线程并发。线程隔离每个线程的变量都是独立的,互不影响。ThreadLocal实战1.常用方法ThreadLocal()构造方法,创建ThreadLocal对象voidset(Tvalue)设置局部变量Tget()获取当前线程绑定的局部变量voidremove()移除当前线程绑定的局部变量2、为什么要使用ThreadLocal首先我们来看一组并发条件下的代码场景:@DatapublicclassThreadLocalTest{privateStringname;publicstaticvoidmain(String[]args){ThreadLocalTesttmp=newThreadLocalTest();for(inti=0;i<4;i++){Threadthread=newThread(()->{tmp.setName(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName()+"\ttake到数据:"+tmp.getName());});thread.setName("Thread-"+i);thread.start();}}}我们理想的代码输出应该是这样的:/**OUTPUT**/Thread-0获取数据:Thread-0Thread-1获取数据:Thread-1Thread-2获取数据:Thread-2Thread-3获取数据:Thread-3但实际输出结果是这样的:/**OUTPUT**/Thread-0获取数据:Thread-1Thread-3gets数据:Thread-3Thread-1获取数据:Thread-1Thread-2获取数据:Thread-2乱序没关系,但是我们可以看到Thread-0获取的值是Thread-1.从结果可以看出,多个线程在访问同一个变量时会出现异常。这是因为线程之间的数据没有隔离!并发线程的问题?然后锁定是不够的!这个时候,你写了如下代码:@DatapublicclassThreadLocalTest{privateStringname;publicstaticvoidmain(String[]args){ThreadLocalTesttmp=newThreadLocalTest();for(inti=0;i<4;i++){Threadthread=newThread(()->{synchronized(tmp){tmp.setName(Thread.currentThread().getName());System.out.println(Thread.currentThread().getName()+"\t"+tmp.getName());}});thread.setName("Thread-"+i);thread.start();}}}/**OUTPUT**/Thread-2Thread-2Thread-3Thread-3Thread-1Thread-1Thread-0Thread-0从结果来看,好像加锁解决了上面的问题,但是synchronized常用于多线程数据共享的问题,而不是多线程数据隔离的问题。这里使用synchronized虽然解决了问题,但是有些不妥,synchronized是重量级锁。为了实现多线程数据隔离,贸然加上synchronized,也会影响性能。加锁的方法也被否定了,那怎么解决呢?为什么不使用ThreadLocal来尝试一下:String[]args){ThreadLocalTesttmp=newThreadLocalTest();for(inti=0;i<4;i++){Threadthread=newThread(()->{tmp.setName(Thread.currentThread().getName());系统.out.println(Thread.currentThread().getName()+"\tgetdata:"+tmp.getName());});thread.setName("Thread-"+i);thread.start();}}}在看输出结果之前,我们先看看代码的变化。首先是多了一个privatestatic修饰的ThreadLocal,然后我们setName的时候,其实上面就是在ThreadLocal中存数据,getName的时候,我们在ThreadLocal中取数据。感觉操作还是挺简单的,但是这样真的能实现线程间的数据隔离吗?我们再看一下结果:/**OUTPUT**/Thread-1获取数据:Thread-1Thread-2获取数据Data:Thread-2Thread-0获取数据:Thread-0Thread-3获取数据:Thread-3从结果可以看出,每个线程都能拿到对应的数据。ThreadLocal也解决了多线程之间的数据隔离问题。那么我们总结一下为什么要使用ThreadLocal,和synchronized有什么区别Synchronized原理:同步机制采用“以时间换空间”的方式,只提供一个变量让不同的线程排队访问。重点:多线程ThreadLocal同步访问资源的原理:ThreadLocal采用“以空间换时间”的方式,为每个线程提供一份变量副本,从而实现同时访问,互不干扰。重点:让每个线程都处于多线程状态3.内部结构从上面的案例我们可以看出ThreadLocal的两个主要方法是set()和get()。那么我们不妨猜测一下,如果我们设计ThreadLocal,我们应该如何设计,是否有这样一种思路:每个ThreadLocal创建一个Map,然后将线程作为Map的key,将要存储的局部变量作为Map的值,这样就可以达到隔离各个线程局部变量的效果。这个想法也是对的。早期的ThreadLocal是这样设计的,但是在JDK8之后设计发生了变化,如下:设计过程:每个Thread线程在ThreadLocalMap内部都有一个ThreadLocalMap,它存储了ThreadLocal对象作为key。线程变量是值。Thread的内部Map由ThreadLocal维护。ThreadLocal负责设置和获取线程的变量值到Map中。对于不同的线程,每次获取一个拷贝值,其他线程无法获取该线程的拷贝值。这样就会形成副本的隔离,互不干扰。注意:每个线程都必须有自己的map,但是这个类是一个普通的java类,没有实现Map接口,但是有类似Map的功能。这个实现似乎比我们之前猜测的要复杂。这样做有什么好处?每个Map存储的Entry数量会减少,因为以前存储数量是由Threads数量决定的,现在是由ThreadMap决定的,ThreadLocals的数量决定了在实际开发中,ThreadLocals的数量小于线程数。当Thread被销毁时,对应的ThreadLocalMap也会被销毁,可以减少内存的使用。4.源码分析首先我们来看一下ThreadLocalMap中的成员:如果你看过HashMap的源码,你一定会觉得这几个很眼熟。其中:INITIAL_CAPACITY:初始容量,必须是2的整数次方扩容,表使用量大于的时候会扩容。ThreadLocalsThread类中有一个ThreadLocal.ThreadLocalMap类型的变量ThreadLocals,用于保存各个线程的私有数据。ThreadLocalMapThreadLocalMap是ThreadLocal的一个内部类,每条数据都是通过Entry来保存的,其中Entry是通过键值对存储的,key是对ThreadLocal的引用。我们可以看到Entry继承自WeakReference,因为如果是强引用,即使ThreadLocal设置为null,GC也不会回收,因为ThreadLocalMap对它有强引用。在不手动删除Entry且CurrentThread还在运行的前提下,一直存在强引用链threadRef->currentThread->threadLocalMap->entry,Entry不会被回收(ThreadLocal实例和值都包含在内)在Entry中),导致entry内存泄漏。那是不是说如果使用了弱引用就不会出现内存泄漏呢?这也是不正确的。因为如果我们不手动删除Entry,此时Entry中的key==null,此时没有对threadLocal实例的强引用,所以threadLocal可以顺利被gc回收,但是value会不会被回收,并且这个块的值永远不会被访问,所以会造成内存泄漏。接下来我们看一下ThreadLocalMap的几个核心方法:set方法。首先我们看源码:publicvoidset(Tvalue){//获取当前线程对象Threadt=Thread。currentThread();//获取本线程对象中维护的ThreadLocalMap对象ThreadLocalMapmap=getMap(t);//判断map是否存在if(map!=null)//如果存在,则调用map.set设置本实体entrymap.set(this,value);else//如果当前线程没有ThreadLocalMap对象,则调用createMap初始化ThreadLocalMap对象//并存储t(当前线程)和value(t对应的值)作为ThreadLocalMap中的firstentrycreateMap(t,value);}ThreadLocalMapgetMap(Threadt){return.threadLocals;}voidcreateMap(Threadt,TfirstValue){//这里就是threadLocalt.threadLocals=newThreadLocalMap(this,firstValue);}执行过程:首先获取当前线程,根据当前线程获取一个map。如果获取的map不为空,给map设置参数(当前ThreadLocal引用作为key)如果Map为空,为线程创建一个map,并设置初始值get方法源码为如下:publicTget(){//获取当前线程对象Threadt=Thread.currentThread();//获取本线程对象中维护的ThreadLocalMap对象ThreadLocalMapmap=getMap(t);//如果这个map存在if(map!=null){//与当前ThreadLocal为key,调用getEntry获取对应的存储实体eThreadLocalMap.Entrye=map.getEntry(this);//对e进行空判断if(e!=null){@SuppressWarnings("unchecked")//获取存储实体e对应的值//就是我们想要的这个ThreadLocal对应的当前线程的值Tresult=(T)e.value;returnresult;}}returnsetInitialValue();}privateTsetInitialValue(){//调用initialValue得到初始化值//该方法可以被子类重写,如果不重写,默认返回nullobjectThreadLocalMapmap=getMap(t);//判断map是否存在if(map!=null)//调用map.set设置这个实体entrymap.set(this,value);else//如果当前线程没有ThreadLocalMap对象,则调用createMap对ThreadLocalMap对象进行初始化//将t(当前线程)和value(t对应的值)作为ThreadLocalMap中的第一项进行存储createMap(t,价值);//返回设置值value返回值;}执行过程:首先获取当前线程,根据当前线程获取map。如果获取的map不为空,则使用ThreadLocal引用作为map中的key,获取map中对应的Entry条目,否则跳转到第4步,如果Entry条目不为空,则返回entry.value,否则跳到第四步。如果map为空或者entry为空,通过initialValue函数获取初始值value,然后使用ThreadLocal引用和value作为firstKey和firstValue新建mapremove方法源码如下:publicvoidremove(){//获取当前线程对象中维护的ThreadLocalMap对象ThreadLocalMapm=getMap(Thread.currentThread());//如果这个map存在if(m!=null)//callmap.removem.remove(this);}//以当前ThreadLocal为key删除对应实体entryprivatevoidremove(ThreadLocal>key){Entry[]tab=table;intlen=tab.length;inti=key.threadLocalHashCode&(len-1);for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){if(e.get()==key){e.clear();expungeStaleEntry(i);return;}}}执行过程:先获取当前线程,根据当前线程获取map。如果获取到的map不为空,则移除当前ThreadLocal对象对应的entryinitialValue。方法源码如下:protectedTInitialValue(){returnnull;}在源代码中,我们可以看到这个方法只是返回null。该方法在线程第一次通过get()方法访问线程的ThreadLocal时被调用。只有当线程先调用set()方法时,initialValue()方法才不会被调用。通常,此方法最多被调用一次。如果希望ThreadLocal线程局部变量的初始值不是null,则必须通过子类化ThreadLocal来覆盖此方法。你可以通过匿名内部类实现。
