0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

什么是ThreadLocal?一文让你彻底掌握ThreadLocal

OSC开源社区 来源:冰河技术 2023-08-07 11:39 次阅读

前言

我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。

b2620dde-32af-11ee-9e74-dac502259ad0.jpg

对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量

什么是ThreadLocal?

ThreadLocal是JDK提供的,支持线程本地变量。也就是说,如果我们创建了一个ThreadLocal变量,则访问这个变量的每个线程都会有这个变量的一个本地副本。如果多个线程同时对这个变量进行读写操作时,实际上操作的是线程自己本地内存中的变量,从而避免了线程安全的问题。

b275f966-32af-11ee-9e74-dac502259ad0.jpg

ThreadLocal使用示例

例如,我们使用ThreadLocal保存并打印相关的变量信息,程序如下所示。

publicclassThreadLocalTest{

privatestaticThreadLocalthreadLocal=newThreadLocal();

publicstaticvoidmain(String[]args){
//创建第一个线程
ThreadthreadA=newThread(()->{
threadLocal.set("ThreadA:"+Thread.currentThread().getName());
System.out.println("线程A本地变量中的值为:"+threadLocal.get());
});
//创建第二个线程
ThreadthreadB=newThread(()->{
threadLocal.set("ThreadB:"+Thread.currentThread().getName());
System.out.println("线程B本地变量中的值为:"+threadLocal.get());
});
//启动线程A和线程B
threadA.start();
threadB.start();
}
}

运行程序,打印的结果信息如下所示。

线程A本地变量中的值为:ThreadA:Thread-0
线程B本地变量中的值为:ThreadB:Thread-1

此时,我们为线程A增加删除ThreadLocal中的变量的操作,如下所示。

publicclassThreadLocalTest{

privatestaticThreadLocalthreadLocal=newThreadLocal();

publicstaticvoidmain(String[]args){
//创建第一个线程
ThreadthreadA=newThread(()->{
threadLocal.set("ThreadA:"+Thread.currentThread().getName());
System.out.println("线程A本地变量中的值为:"+threadLocal.get());
threadLocal.remove();
System.out.println("线程A删除本地变量后ThreadLocal中的值为:"+threadLocal.get());
});
//创建第二个线程
ThreadthreadB=newThread(()->{
threadLocal.set("ThreadB:"+Thread.currentThread().getName());
System.out.println("线程B本地变量中的值为:"+threadLocal.get());
System.out.println("线程B没有删除本地变量:"+threadLocal.get());
});
//启动线程A和线程B
threadA.start();
threadB.start();
}
}

此时的运行结果如下所示。

线程A本地变量中的值为:ThreadA:Thread-0
线程B本地变量中的值为:ThreadB:Thread-1
线程B没有删除本地变量:ThreadB:Thread-1
线程A删除本地变量后ThreadLocal中的值为:null

通过上述程序我们可以看出,线程A和线程B存储在ThreadLocal中的变量互不干扰,线程A存储的变量只能由线程A访问,线程B存储的变量只能由线程B访问。

ThreadLocal原理

首先,我们看下Thread类的源码,如下所示。

publicclassThreadimplementsRunnable{
/***********省略N行代码*************/
ThreadLocal.ThreadLocalMapthreadLocals=null;
ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;
/***********省略N行代码*************/
}

由Thread类的源码可以看出,在ThreadLocal类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。

这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程的threadLocals变量里面的。也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。

接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。

set()方法

set()方法的源代码如下所示。

publicvoidset(Tvalue){
//获取当前线程
Threadt=Thread.currentThread();
//以当前线程为Key,获取ThreadLocalMap对象
ThreadLocalMapmap=getMap(t);
//获取的ThreadLocalMap对象不为空
if(map!=null)
//设置value的值
map.set(this,value);
else
//获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量
createMap(t,value);
}

在set()方法中,首先获取调用set()方法的线程,接下来,使用当前线程作为Key调用getMap(t)方法来获取ThreadLocalMap对象,getMap(Thread t)的方法源码如下所示。

ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}

可以看到,getMap(Thread t)方法获取的是线程变量自身的threadLocals成员变量。

在set()方法中,如果调用getMap(t)方法返回的对象不为空,则把value值设置到Thread类的threadLocals成员变量中,而传递的key为当前ThreadLocal的this对象,value就是通过set()方法传递的值。

如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。

voidcreateMap(Threadt,TfirstValue){
t.threadLocals=newThreadLocalMap(this,firstValue);
}

也就是创建当前线程的threadLocals变量。

get()方法

get()方法的源代码如下所示。

publicTget(){
//获取当前线程
Threadt=Thread.currentThread();
//获取当前线程的threadLocals成员变量
ThreadLocalMapmap=getMap(t);
//获取的threadLocals变量不为空
if(map!=null){
//返回本地变量对应的值
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
Tresult=(T)e.value;
returnresult;
}
}
//初始化threadLocals成员变量的值
returnsetInitialValue();
}

通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。

privateTsetInitialValue(){
//调用初始化Value的方法
Tvalue=initialValue();
Threadt=Thread.currentThread();
//根据当前线程获取threadLocals成员变量
ThreadLocalMapmap=getMap(t);
if(map!=null)
//threadLocals不为空,则设置value值
map.set(this,value);
else
//threadLocals为空,创建threadLocals变量
createMap(t,value);
returnvalue;
}

其中,initialValue()方法的源码如下所示。

protectedTinitialValue(){
returnnull;
}

通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。

remove()方法

remove()方法的源代码如下所示。

publicvoidremove(){
//根据当前线程获取threadLocals成员变量
ThreadLocalMapm=getMap(Thread.currentThread());
if(m!=null)
//threadLocals成员变量不为空,则移除value值
m.remove(this);
}

remove()方法的实现比较简单,首先根据当前线程获取threadLocals成员变量,不为空,则直接移除value的值。

注意:如果调用线程一致不终止,则本地变量会一直存放在调用线程的threadLocals成员变量中,所以,如果不需要使用本地变量时,可以通过调用ThreadLocal的remove()方法,将本地变量从当前线程的threadLocals成员变量中删除,以免出现内存溢出的问题。

ThreadLocal变量不具有传递性

使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。

接下来,我们来看一段代码,如下所示。

publicclassThreadLocalTest{

privatestaticThreadLocalthreadLocal=newThreadLocal();

publicstaticvoidmain(String[]args){
//在主线程中设置值
threadLocal.set("ThreadLocalTest");
//在子线程中获取值
Threadthread=newThread(newRunnable(){
@Override
publicvoidrun(){
System.out.println("子线程获取值:"+threadLocal.get());
}
});
//启动子线程
thread.start();
//在主线程中获取值
System.out.println("主线程获取值:"+threadLocal.get());
}
}

运行这段代码输出的结果信息如下所示。

主线程获取值:ThreadLocalTest
子线程获取值:null

通过上述程序,我们可以看出在主线程中向ThreadLocal设置值后,在子线程中是无法获取到这个值的。那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。

InheritableThreadLocal使用示例

InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值,例如,我们将ThreadLocalTest类中的threadLocal静态变量改写成InheritableThreadLocal类的实例,如下所示。

publicclassThreadLocalTest{

privatestaticThreadLocalthreadLocal=newInheritableThreadLocal();

publicstaticvoidmain(String[]args){
//在主线程中设置值
threadLocal.set("ThreadLocalTest");
//在子线程中获取值
Threadthread=newThread(newRunnable(){
@Override
publicvoidrun(){
System.out.println("子线程获取值:"+threadLocal.get());
}
});
//启动子线程
thread.start();
//在主线程中获取值
System.out.println("主线程获取值:"+threadLocal.get());
}
}

此时,运行程序输出的结果信息如下所示。

主线程获取值:ThreadLocalTest
子线程获取值:ThreadLocalTest

可以看到,使用InheritableThreadLocal类存储本地变量时,子线程能够获取到父线程中设置的本地变量。

b2ce9eae-32af-11ee-9e74-dac502259ad0.jpg

InheritableThreadLocal原理

首先,我们来看下InheritableThreadLocal类的源码,如下所示。

publicclassInheritableThreadLocalextendsThreadLocal{
protectedTchildValue(TparentValue){
returnparentValue;
}

ThreadLocalMapgetMap(Threadt){
returnt.inheritableThreadLocals;
}

voidcreateMap(Threadt,TfirstValue){
t.inheritableThreadLocals=newThreadLocalMap(this,firstValue);
}
}

由InheritableThreadLocal类的源代码可知,InheritableThreadLocal类继承自ThreadLocal类,并且重写了ThreadLocal类的childValue()方法、getMap()方法和createMap()方法。也就是说,当调用ThreadLocal的set()方法时,创建的是当前Thread线程的inheritableThreadLocals成员变量而不再是threadLocals成员变量。

这里,我们需要思考一个问题:InheritableThreadLocal类的childValue()方法是何时被调用的呢?这就需要我们来看下Thread类的构造方法了,如下所示。

publicThread(){
init(null,null,"Thread-"+nextThreadNum(),0);
}

publicThread(Runnabletarget){
init(null,target,"Thread-"+nextThreadNum(),0);
}

Thread(Runnabletarget,AccessControlContextacc){
init(null,target,"Thread-"+nextThreadNum(),0,acc,false);
}

publicThread(ThreadGroupgroup,Runnabletarget){
init(group,target,"Thread-"+nextThreadNum(),0);
}

publicThread(Stringname){
init(null,null,name,0);
}

publicThread(ThreadGroupgroup,Stringname){
init(group,null,name,0);
}

publicThread(Runnabletarget,Stringname){
init(null,target,name,0);
}

publicThread(ThreadGroupgroup,Runnabletarget,Stringname){
init(group,target,name,0);
}

publicThread(ThreadGroupgroup,Runnabletarget,Stringname,
longstackSize){
init(group,target,name,stackSize);
}

可以看到,Thread类的构造方法最终调用的是init()方法,那我们就来看下init()方法,如下所示。

privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,
longstackSize,AccessControlContextacc,
booleaninheritThreadLocals){
/************省略部分源码************/
if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)
this.inheritableThreadLocals=
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/*StashthespecifiedstacksizeincasetheVMcares*/
this.stackSize=stackSize;

/*SetthreadID*/
tid=nextThreadID();
}

可以看到,在init()方法中会判断传递的inheritThreadLocals变量是否为true,同时父线程中的inheritableThreadLocals是否为null,如果传递的inheritThreadLocals变量为true,同时,父线程中的inheritableThreadLocals不为null,则调用ThreadLocal类的createInheritedMap()方法。

staticThreadLocalMapcreateInheritedMap(ThreadLocalMapparentMap){
returnnewThreadLocalMap(parentMap);
}

在createInheritedMap()中,使用父线程的inheritableThreadLocals变量作为参数创建新的ThreadLocalMap对象。然后在Thread类的init()方法中会将这个ThreadLocalMap对象赋值给子线程的inheritableThreadLocals成员变量。

接下来,我们来看看ThreadLocalMap的构造函数都干了啥,如下所示。

privateThreadLocalMap(ThreadLocalMapparentMap){
Entry[]parentTable=parentMap.table;
intlen=parentTable.length;
setThreshold(len);
table=newEntry[len];

for(intj=0;j< len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocalkey=(ThreadLocal)e.get();
if(key!=null){
//调用重写的childValue方法
Objectvalue=key.childValue(e.value);
Entryc=newEntry(key,value);
inth=key.threadLocalHashCode&(len-1);
while(table[h]!=null)
h=nextIndex(h,len);
table[h]=c;
size++;
}
}
}
}

在ThreadLocalMap的构造函数中,调用了InheritableThreadLocal类重写的childValue()方法。而InheritableThreadLocal类通过重写getMap()方法和createMap()方法,让本地变量保存到了Thread线程的inheritableThreadLocals变量中,线程通过InheritableThreadLocal类的set()方法和get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。

此时,如果父线程创建子线程,在Thread类的构造函数中会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量中。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 存储器
    +关注

    关注

    38

    文章

    7482

    浏览量

    163756
  • Thread编程
    +关注

    关注

    0

    文章

    3

    浏览量

    878
  • 内存溢出
    +关注

    关注

    0

    文章

    10

    浏览量

    1195

原文标题:一文让你彻底掌握ThreadLocal

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    彻底掌握MOS管

    基础知识中 MOS 部分迟迟未整理,实际分享的电路中大部分常用电路都用到了MOS管, 今天势必要来篇文章,彻底掌握mos管!
    发表于 07-05 11:56 3w次阅读

    ThreadLocal实例应用

    ThreadLocal相信大家都用过,但知道他的原理吗,今天了不起带大家学习ThreadLocalThreadLocal是什么 在多线程编程中,经常会遇到需要在不同线程中共享数据
    的头像 发表于 09-30 10:19 654次阅读
    <b class='flag-5'>ThreadLocal</b>实例应用

    ThreadLocal的定义、用法及优点

    ThreadLocal 简介 ThreadLocal是Java中个非常重要的线程技术。它可以每个线程都拥有自己的变量副本,避免了线程间的竞争和数据泄露问题。在本文中,我们将详细介绍
    的头像 发表于 09-30 10:14 1052次阅读
    <b class='flag-5'>ThreadLocal</b>的定义、用法及优点

    彻底理解DFT

    从本期开始,E课网将为大家推出系列的‘彻底理解’专题。该专题的目的是大家更好的理解并弄清楚设计过程中所面临的各种问题,在对问题充分理
    发表于 05-25 15:32

    份白皮书,彻底了解比特币

    份白皮书,彻底了解比特币
    发表于 03-05 15:02 26次下载
    <b class='flag-5'>一</b>份白皮书,<b class='flag-5'>让</b><b class='flag-5'>你</b><b class='flag-5'>彻底</b>了解比特币

    8张图彻底理解晶体管开关电路图

    8张彻底理解晶体管开关电路图
    的头像 发表于 02-05 12:41 1.1w次阅读

    ThreadLocal发生内存泄漏的原因

    前言 ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同个线程内多个函数或者组件之间些公共变量的传递的复杂度。但是如果滥用 ThreadLoca
    的头像 发表于 05-05 16:23 3672次阅读

    如何使用ThreadLocal来避免内存泄漏

    本次给大家介绍重要的工具ThreadLocal。讲解内容如下,同时介绍什么场景下发生内存泄漏,如何复现内存泄漏,如何正确使用它来避免内存泄漏。 ThreadLocal是什么?有哪些用途
    的头像 发表于 08-20 09:29 4222次阅读
    如何使用<b class='flag-5'>ThreadLocal</b>来避免内存泄漏

    FastThreadLocal快在哪里

    netty还要自己造个FastThreadLocal?FastThreadLocal快在哪里? 这需要从jdk ThreadLocal的本身说起。如下图: 在java线程中,每个线程都有
    的头像 发表于 09-13 09:17 1338次阅读

    ThreadLocal的作用以及应用场景

    个简单的例子:目前有100个学生等待签字,但是老师只有个笔,那老师只能按顺序的分给每个学生,等待A学生签字完成然后将笔交给B学生,这就类似Lock,Synchronized的方式。而ThreadLocal是,老师直接拿出一
    的头像 发表于 09-19 10:56 1346次阅读

    ThreadLocal源码解析及实战应用

    ThreadLocal个关于创建线程局部变量的类。
    的头像 发表于 01-29 14:53 460次阅读

    ThreadLocal是什么

    和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式。
    的头像 发表于 01-30 11:36 1458次阅读

    ThreadLocal父子线程之间该如何传递数据?

    比如会有以下的这种代码的实现。在子线程中调用 get 时,我们拿到的 Thread 对象是当前子线程对象,对吧,每个线程都有自己独立的 ThreadLocal,那么当前子线程
    的头像 发表于 02-20 11:26 903次阅读

    掌握Linux常用命令

    掌握Linux40个命令
    的头像 发表于 04-03 11:38 645次阅读

    ThreadLocal基本内容与用法

    下面我们就来看看道哥都用的ThreadLocal。 1 ThreadLocal来自哪里 Since : 1.2 Author : Josh Bloch and Doug Lea 又是并发大佬们
    的头像 发表于 10-13 11:39 457次阅读