全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-11-28_Java 多线程为啥要有ThreadLocal,怎么用,这篇讲全了!

您的位置:首页 >> 新闻 >> 行业资讯

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变量操作示例--增删查源代码 阅读原文

上一篇:2022-10-17_认真的吗?让机器狗当守门员,还发了篇论文 下一篇:2021-09-29_​新世相这支短片 , 诠释了迪士尼的公主力

TAG标签:

19
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价