Java 多线程为啥要有ThreadLocal,怎么用,这篇讲全了!
本文为掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前面我们学习的线程并发时的同步控制,是为了保证多个线程对共享数据争用时的正确性的。那如果一个操作本身不涉及对共享数据的使用,相反,只是希望变量只能由创建它的线程使用(即线程隔离)就需要到线程本地存储了。
Java 通过ThreadLocal提供了程序对线程本地存储的使用。
通过创建ThreadLocal类的实例,让我们能够创建只能由同一线程读取和写入的变量。因此,即使两个线程正在执行相同的代码,并且代码引用了相同名称的ThreadLocal变量,这两个线程也无法看到彼此的存储在ThreadLocal里的值。否则也就不能叫线程本地存储了。
本文大纲如下:
ThreadLocalThreadLocal是 Java 内置的类,全称java.lang.ThreadLoal,java.lang包里定义的类和接口在程序里都是可以直接使用,不需要导入的。
ThreadLocal的类定义如下:
publicclassThreadLocalT{
publicTget(){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
//......
returnsetInitialValue();
}
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null){
map.set(this,value);
}else{
createMap(t,value);
}
}
publicvoidremove(){
ThreadLocalMapm=getMap(Thread.currentThread());
if(m!=null){
m.remove(this);
}
}
protectedTinitialValue(){
returnnull;
}
publicstaticSThreadLocalwithInitial(Supplier?extendsSsupplier){
returnnewSuppliedThreadLocal(supplier);
}
//...
}
上面只是列出了ThreadLocal类里我们经常会用到的方法,这几个方法他们的说明如下。
T get()- 用于获取ThreadLocal在当前线程中保存的变量副本。void set(T value)- 用于向ThreadLocal中设置当前线程中变量的副本。void remove()- 用于删除当前线程保存在ThreadLocal中的变量副本。initialValue()- 为ThreadLocal设置默认的get方法获取到的始值,默认是 null ,想修改的话需要用子类重写 initialValue 方法,或者是用TheadLocal提供的withInitial方法 。下面我们详细看一下ThreadLocal的使用。
创建和读写 ThreadLocal通过上面ThreadLocal类的定义我们能看出来,ThreadLocal是支持泛型的,所以在创建ThreadLocal时没有什么特殊需求的情况下,我们都会为其提供类型参数,这样在读取使用ThreadLocal变量时就能免去类型转换的操作。
privateThreadLocalthreadLocal=newThreadLocal();
threadLocal.set("Athreadlocalvalue");
//创建时没有使用泛型指定类型,默认是Object
//使用时要先做类型转换
StringthreadLocalValue=(String)threadLocal.get();
上面这个例子,在创建ThreadLocal时没有使用泛型指定类型,所以存储在其中的值默认是Object类型,这样就需要在使用时先做类型转换才行。
下面再看一个使用泛型的版本
privateThreadLocalStringmyThreadLocal=newThreadLocalString
myThreadLocal.set("HelloThreadLocal");
StringthreadLocalValue=myThreadLocal.get();
现在我们只能把String类型的值存到ThreadLocal中,并且从ThreadLocal读取出值后也不再需要进行类型转换。
关于泛型使用方面的详细讲解,可以看本系列中的泛型章节。
看了这篇Java 泛型通关指南,再也不怵满屏尖括号了
想要删除一个ThreadLocal实例里存储的值,只需要调用ThreadLocal实例中的remove方法即可。
myThreadLocal.remove();
当然,这个删除操作只是删除的变量在本地线程中的副本,其他线程不会受到本线程中删除操作的影响。下面我们把ThreadLocal的创建、读写和删除攒一个简单的例子,做下演示。
//源码:https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/ThreadLocalExample.java
packagecom.threadlocal;
publicclassThreadLocalExample{
privateThreadLocalIntegerthreadLocal=newThreadLocal();
privatevoidsetAndPrintThreadLocal(){
threadLocal.set((int)(Math.random()*100D)
try{
Thread.sleep(2000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+threadLocal.get()
if(threadLocal.get()%2==0){
//测试删除ThreadLocal
System.out.println(Thread.currentThread().getName()+":删除ThreadLocal");
threadLocal.remove();
}
}
publicstaticvoidmain(String[]args)throwsInterruptedException{
ThreadLocalExampletlExample=newThreadLocalExample();
Threadthread1=newThread(()-tlExample.setAndPrintThreadLocal(),"线程1");
Threadthread2=newThread(()-tlExample.setAndPrintThreadLocal(),"线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
上面的例程会有如下输出,当然如果恰好两个线程里ThreadLocal变量里存储的都是偶数的话,就不会有第三行输出啦。
线程2:97
线程1:64
线程1:删除ThreadLocal
本例子的源码项目放在了GitHub上,需要的可自行取用进行参考:ThreadLocal变量操作示例--增删查
为 ThreadLocal 设置初始值在程序里,声明ThreadLocal类型的变量时,我们可以同时为变量设置一个自定义的初始值,这样做的好处是,即使没有使用set方法给ThreadLocal变量设置值的情况下,调用ThreadLocal变量的get()时能返回一个对业务逻辑来说更有意义的初始值,而不是默认的 Null 值。
在 Java 中有两种方式可以指定ThreadLocal变量的自定义初始值:
创建一个ThreadLocal的子类,覆盖initialValue()方法,程序中则使用ThreadLocal子类创建实例变量。使用ThreadLocal类提供的的静态方法withInitial(Supplier? extends S supplier)来创建ThreadLocal实例变量,该方法接收一个函数式接口Supplier的实现作为参数,在Supplier实现中为ThreadLocal设置初始值。关于函数式接口Supplier如果你还不太清楚的话,可以查看系列中函数式编程接口章节中的详细内容。下面我们看看分别用这两种方式怎么给ThreadLocal变量提供初始值。
使用子类覆盖 initialValue() 设置初始值通过定义ThreadLocal的子类,在子类中覆盖initialValue()方法的方式给ThreadLocal变量设置初始值的方式,可以使用匿名类,简化创建子类的步骤。
下面我们在程序里创建ThreadLocal实例时,直接使用匿名类来覆盖initialValue()方法的一个例子。
publicclassThreadLocalExample{
privateThreadLocalthreadLocal=newThreadLocalInteger(){
@OverrideprotectedIntegerinitialValue(){
return(int)System.currentTimeMillis();
}
......
}
有同学可能会问,这块能不能用 Lambda 而不是用匿名类,答案是不能,在这个专栏讲 Lambda 的文章中我们说过,Lambda 只能用于实现函数式接口(接口中有且只有一个抽象方法,所以这里只能使用匿名了简化创建子类的步骤,不过另外一种通过withInitial方法创建并自定义初始化ThreadLocal变量的时候,是可以使用Lambda 的,我们下面看看使用withInital静态方法设置ThreadLocal变量初始值的演示。
通过 withInital 静态方法设置初始值为ThreadLocal实例变量指定初始值的第二种方式是使用ThreadLocal类提供的静态工厂方法withInitial。withInitial方法接收一个函数式接口Supplier的实现作为参数,在Supplier的实现中我们可以为要创建的ThreadLocal变量设置初始值。
Supplier 接口是一个函数式接口,表示提供某种值的函数。 Supplier 接口也可以被认为是工厂接口。
@FunctionalInterface public interface Supplier{ T get(); }
下面的程序里,我们用 ThreadLocal 的 withInitial 方法为 ThreadLocal 实例变量设置了初始值
publicclassThreadLocalExample{
privateThreadLocalIntegerthreadLocal=ThreadLocal.withInitial(newSupplierInteger(){
@Override
publicStringget(){
return(int)System.currentTimeMillis();
}
......
}
对于函数式接口,理所当然会想到用Lambda来实现。上面这个withInitial的例子用Lambda实现的话能进一步简化成:
publicclassThreadLocalExample{
privateThreadLocalIntegerthreadLocal=ThreadLocal.withInitial(()-(int)System.currentTimeMillis());
......
}
关于 Lambda 和 函数式接口 Supplier 的详细内容,可以通过本系列中与这两个主题相关的文章进行学习。
Java Lambda 表达式的各种形态和使用场景,看这篇就够了Java 中那些绕不开的内置接口 -- 函数式编程和 Java 的内置函数式接口ThreadLocal 在父子线程间的传递ThreadLocal 提供的线程本地存储,给数据提供了线程隔离,但是有的时候用一个线程开启的子线程,往往是需要些相关性的,那么父线程的ThreadLocal中存储的数据能在子线程中使用吗?答案是不行......那怎么能让父子线程上下文能关联起来,Java 为这种情况专门提供了InheritableThreadLocal给我们使用。
InheritableThreadLocal是ThreadLocal的一个子类,其定义如下:
publicclassInheritableThreadLocalTextendsThreadLocalT{
protectedTchildValue(TparentValue){
returnparentValue;
}
/**
*GetthemapassociatedwithaThreadLocal.
*
*@paramtthecurrentthread
*/
ThreadLocalMapgetMap(Threadt){
returnt.inheritableThreadLocals;
}
/**
*CreatethemapassociatedwithaThreadLocal.
*
*@paramtthecurrentthread
*@paramfirstValuevaluefortheinitialentryofthetable.
*/
voidcreateMap(Threadt,TfirstValue){
t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);
}
}
与ThreadLocal让线程拥有变量在本地存储的副本这个形式不同的是,InheritableThreadLocal允许让创建它的线程和其子线程都能访问到在它里面存储的值。
下面是一个InheritableThreadLocal的使用示例
//源码:https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/InheritableThreadLocalExample.java
packagecom.threadlocal;
publicclassInheritableThreadLocalExample{
publicstaticvoidmain(String[]args){
ThreadLocalStringthreadLocal=newThreadLocal();
InheritableThreadLocalStringinheritableThreadLocal=
newInheritableThreadLocal();
Threadthread1=newThread(()-{
System.out.println("=====Thread1=====");
threadLocal.set("Thread1-ThreadLocal");
inheritableThreadLocal.set("Thread1-InheritableThreadLocal");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
ThreadchildThread=newThread(()-{
System.out.println("=====ChildThread=====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
childThread.start();
thread1.start();
Threadthread2=newThread(()-{
try{
Thread.sleep(3000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("=====Thread2=====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
thread2.start();
}
}
运行程序后,会有如下输出
=====Thread1=====
Thread1-ThreadLocal
Thread1-InheritableThreadLocal
=====ChildThread=====
null
Thread1-InheritableThreadLocal
=====Thread2=====
null
null
这个例程中创建了分别创建了ThreadLocal和InheritableThreadLocal的 实例,然后例程中创建的线程Thread1, 在线程Thread1中向ThreadLocal和InheritableThreadLocal实例中都存储了数据,并尝试在开启了的子线程ChildThread中访问这两个数据。按照上面的解释,ChildThread应该只能访问到父线程存储在InheritableThreadLocal实例中的数据。
在例程的最后,程序又创建了一个与Thread1不相干的线程Thread2, 它在访问ThreadLocal和InheritableThreadLocal实例中存储的数据时,因为它自己没有设置过,所以最后得到的结果都是null。
ThreadLocal 的实现原理梳理完ThreadLocal相关的常用功能都怎么使用后,我们再来简单过一下ThreadLocal在 Java 中的实现原理。
在Thread类中维护着一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals。这个成员变量就是用来存储当前线程独占的变量副本的。
publicclassThreadimplementsRunnable{
//...
ThreadLocal.ThreadLocalMapthreadLocals=null;
//...
}
ThreadLocalMap类 是ThreadLocal中的静态内部类,其定义如下。
packagejava.lang;
publicclassThreadLocalT{
//...
staticclassThreadLocalMap{
//...
staticclassEntryextendsWeakReferenceThreadLocal{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;
Entry(ThreadLocalk,Objectv){
super(k);
value=
}
}
//...
}
}
它维护着一个Entry数组,Entry继承了WeakReference,所以是弱引用。Entry用于保存键值对,其中:
key是ThreadLocal对象;value是传递进来的对象(变量副本)。ThreadLocalMap虽然是类似HashMap结构的数据结构,但它解决哈希碰撞的时候,使用的方案并非像HashMap那样使用拉链法(用链表保存冲突的元素)。
实际上,ThreadLocalMap采用了线性探测的方式来解决哈希碰撞冲突。所谓线性探测,就是根据初始key的hashcode值确定元素在哈希表数组中的位置,如果发现这个位置上已经被其他的key值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
总结关于 ThreadLocal 的内容就介绍到这了,这块内容在一些基础的面试中还是挺常被问到的,与它一起经常被问到的还有一个 volatile 关键字,这部分内容我们放到下一篇再讲,喜欢本文的内容还请给点个赞,点个关注,这样就能及时跟上后面的更新啦。
引用链接Java并发编程--多线程间的同步控制和通信看了这篇Java 泛型通关指南,再也不怵满屏尖括号了Java Lambda 表达式的各种形态和使用场景,看这篇就够了Java 中那些绕不开的内置接口 -- 函数式编程和 Java 的内置函数式接口ThreadLocal变量操作示例--增删查源代码
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线