【高并发】一文带你彻底搞懂threadlocal-凯发k8国际娱乐官网入口

举报
发表于 2023/07/31 11:18:07 2023/07/31
【摘要】 对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而threadlocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量。

大家好,我是冰河~~

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

图片.png

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

什么是threadlocal?

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

图片.png

threadlocal使用示例

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

public class threadlocaltest {
    private static threadlocal<string> threadlocal = new threadlocal<string>();
    public static void main(string[] args){
        //创建第一个线程
        thread threada = new thread(()->{
            threadlocal.set("threada:"  thread.currentthread().getname());
            system.out.println("线程a本地变量中的值为:"  threadlocal.get());
        });
        //创建第二个线程
        thread threadb = new thread(()->{
            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中的变量的操作,如下所示。

public class threadlocaltest {
    private static threadlocal<string> threadlocal = new threadlocal<string>();
    public static void main(string[] args){
        //创建第一个线程
        thread threada = new thread(()->{
            threadlocal.set("threada:"  thread.currentthread().getname());
            system.out.println("线程a本地变量中的值为:"  threadlocal.get());
            threadlocal.remove();
            system.out.println("线程a删除本地变量后threadlocal中的值为:"  threadlocal.get());
        });
        //创建第二个线程
        thread threadb = new thread(()->{
            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访问。

图片.png

threadlocal原理

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

public class thread implements runnable {
    /***********省略n行代码*************/
    threadlocal.threadlocalmap threadlocals = null;
    threadlocal.threadlocalmap inheritablethreadlocals = 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()方法的源代码如下所示。

public void set(t value) {
    //获取当前线程
    thread t = thread.currentthread();
    //以当前线程为key,获取threadlocalmap对象
    threadlocalmap map = 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)的方法源码如下所示。

threadlocalmap getmap(thread t) {
    return t.threadlocals;
}

可以看到,getmap(thread t)方法获取的是线程变量自身的threadlocals成员变量。

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

如果调用getmap(t)方法返回的对象为空,则程序调用createmap(t, value)方法来实例化thread类的threadlocals成员变量。

void createmap(thread t, t firstvalue) {
    t.threadlocals = new threadlocalmap(this, firstvalue);
}

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

get()方法

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

public t get() {
    //获取当前线程
    thread t = thread.currentthread();
    //获取当前线程的threadlocals成员变量
    threadlocalmap map = getmap(t);
    //获取的threadlocals变量不为空
    if (map != null) {
        //返回本地变量对应的值
        threadlocalmap.entry e = map.getentry(this);
        if (e != null) {
            @suppresswarnings("unchecked")
            t result = (t)e.value;
            return result;
        }
    }
    //初始化threadlocals成员变量的值
    return setinitialvalue();
}

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

private t setinitialvalue() {
    //调用初始化value的方法
    t value = initialvalue();
    thread t = thread.currentthread();
    //根据当前线程获取threadlocals成员变量
    threadlocalmap map = getmap(t);
    if (map != null)
        //threadlocals不为空,则设置value值
        map.set(this, value);
    else
        //threadlocals为空,创建threadlocals变量
        createmap(t, value);
    return value;
}

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

protected t initialvalue() {
    return null;
}

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

remove()方法

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

public void remove() {
    //根据当前线程获取threadlocals成员变量
    threadlocalmap m = getmap(thread.currentthread());
    if (m != null)
        //threadlocals成员变量不为空,则移除value值
        m.remove(this);
}

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

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

图片.png

threadlocal变量不具有传递性

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

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

public class threadlocaltest {
    private static threadlocal<string> threadlocal = new threadlocal<string>();
    public static void main(string[] args){
        //在主线程中设置值
        threadlocal.set("threadlocaltest");
        //在子线程中获取值
        thread thread = new thread(new runnable() {
            @override
            public void run() {
                system.out.println("子线程获取值:"  threadlocal.get());
            }
        });
        //启动子线程
        thread.start();
        //在主线程中获取值
        system.out.println("主线程获取值:"  threadlocal.get());
    }
}

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

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

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

inheritablethreadlocal使用示例

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

public class threadlocaltest {
    private static threadlocal<string> threadlocal = new inheritablethreadlocal<string>();
    public static void main(string[] args){
        //在主线程中设置值
        threadlocal.set("threadlocaltest");
        //在子线程中获取值
        thread thread = new thread(new runnable() {
            @override
            public void run() {
                system.out.println("子线程获取值:"  threadlocal.get());
            }
        });
        //启动子线程
        thread.start();
        //在主线程中获取值
        system.out.println("主线程获取值:"  threadlocal.get());
    }
}

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

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

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

图片.png

inheritablethreadlocal原理

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

public class inheritablethreadlocal<t> extends threadlocal<t> {
    protected t childvalue(t parentvalue) {
        return parentvalue;
    }
    threadlocalmap getmap(thread t) {
       return t.inheritablethreadlocals;
    }
    void createmap(thread t, t firstvalue) {
        t.inheritablethreadlocals = new threadlocalmap(this, firstvalue);
    }
}

由inheritablethreadlocal类的源代码可知,inheritablethreadlocal类继承自threadlocal类,并且重写了threadlocal类的childvalue()方法、getmap()方法和createmap()方法。也就是说,当调用threadlocal的set()方法时,创建的是当前thread线程的inheritablethreadlocals成员变量而不再是threadlocals成员变量。

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

public thread() {
     init(null, null, "thread-"  nextthreadnum(), 0);
 }
public thread(runnable target) {
    init(null, target, "thread-"  nextthreadnum(), 0);
}
thread(runnable target, accesscontrolcontext acc) {
    init(null, target, "thread-"  nextthreadnum(), 0, acc, false);
}
public thread(threadgroup group, runnable target) {
    init(group, target, "thread-"  nextthreadnum(), 0);
}
public thread(string name) {
    init(null, null, name, 0);
}
public thread(threadgroup group, string name) {
    init(group, null, name, 0);
}
public thread(runnable target, string name) {
    init(null, target, name, 0);
}
public thread(threadgroup group, runnable target, string name) {
    init(group, target, name, 0);
}
public thread(threadgroup group, runnable target, string name,
              long stacksize) {
    init(group, target, name, stacksize);
}

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

private void init(threadgroup g, runnable target, string name,
                      long stacksize, accesscontrolcontext acc,
                      boolean inheritthreadlocals) {
       /************省略部分源码************/
        if (inheritthreadlocals && parent.inheritablethreadlocals != null)
            this.inheritablethreadlocals =
                threadlocal.createinheritedmap(parent.inheritablethreadlocals);
        /* stash the specified stack size in case the vm cares */
        this.stacksize = stacksize;
        /* set thread id */
        tid = nextthreadid();
    }

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

static threadlocalmap createinheritedmap(threadlocalmap parentmap) {
    return new threadlocalmap(parentmap);
}

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

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

private threadlocalmap(threadlocalmap parentmap) {
    entry[] parenttable = parentmap.table;
    int len = parenttable.length;
    setthreshold(len);
    table = new entry[len];
    for (int j = 0; j < len; j) {
        entry e = parenttable[j];
        if (e != null) {
            @suppresswarnings("unchecked")
            threadlocal<object> key = (threadlocal<object>) e.get();
            if (key != null) {
                //调用重写的childvalue方法
                object value = key.childvalue(e.value);
                entry c = new entry(key, value);
                int h = 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变量中。

如果觉得文章对你有点帮助,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发编程技术。

最后,附上并发编程需要掌握的核心技能知识图,祝大家在学习并发编程时,少走弯路。

图片.png

写在最后

如果你想进大厂,想升职加薪,或者对自己现有的工作比较迷茫,都可以私信我交流,希望我的一些经历能够帮助到大家~~

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,我是冰河,我们下期见~~

【凯发k8国际娱乐官网入口的版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200