深入理解ThreadLocal原来及与ThreadLocal使用注意事项

KevinQ 发表于 2022/04/29 17:51:59 2022/04/29
【摘要】 学习ThreadLocal起因学习ThreadLocal起源于最近学习的两个框架:若依开源系统,以及权限验证的开源框架Shiro。在若依开源系统中,其分页插件:PageHelper的部分核心代码中,有:package com.github.pagehelper.page;public abstract class PageMethod { protected static final...

学习ThreadLocal起因

学习ThreadLocal起源于最近学习的两个框架:若依开源系统,以及权限验证的开源框架Shiro。

在若依开源系统中,其分页插件:PageHelper的部分核心代码中,有:

package com.github.pagehelper.page;

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

    public static <T> Page<T> getLocalPage() {
        return (Page)LOCAL_PAGE.get();
    }

    public static void clearPage() {
        LOCAL_PAGE.remove();
    }
    
    // 省略的其他代码...
}

而shiro核心版块中有:

public abstract class ThreadContext {
    private static final ThreadLocal<Map<Object, Object>> resources = new ThreadContext.InheritableThreadLocalMap();

    protected ThreadContext() {
    }

    public static Map<Object, Object> getResources() {
        return (Map)(resources.get() == null ? Collections.emptyMap() : new HashMap((Map)resources.get()));
    }

    public static void setResources(Map<Object, Object> newResources) {
        if (!CollectionUtils.isEmpty(newResources)) {
            ensureResourcesInitialized();
            ((Map)resources.get()).clear();
            ((Map)resources.get()).putAll(newResources);
        }
    }
}

其中,都用到一个非常强大的类:ThreadLocal。

在PageHelper中,使用TheadLocal来保存分页参数Page,在Shiro中,使用ThreadLocal保存了一个map<Object, Object>对象Resources,根据上下文可以看出shiro中的ThreadLocal保存了当前登录账号,或者说当前登录对象的信息,即shiro中的核心对象subject。

ThreadLocal给予了我们为当前线程局部保存变量的能力,换一个角度,ThreadLocal可以做到线程的数据隔离。

ThreadLocal的原理

只有特定线程能取出特定线程的数据,这一点我们很容易联想到ThreadLocal的实现原理:构造一个Map映射对象,以Thread为键值key,以特定的值为存储的值,从而实现每次取值只能取当前线程保存的值。

ThreadLocal的两个重要的api为get,set,可以通过其源码看到这一原理的实现方式:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

其中构造了一个特定的映射对象ThreadLocalMap,我们不再赘述。

ThreadLocal可以存储线程执行上下文信息,简化一些线程中方法调用栈参数逐层传递的问题,比如我们在文章《拦截器中巧用ThreadLocal规避层层传值》中提到的,使用ThreadLocal保存请求Request,从而在接口内部可以直接获取当前请求信息,利用这一点可以做很多事情,例如我们可以抛开shiro做自己定制的权限验证系统。

ThreadLocal也解决了某些线程不安全问题,例如时间日期格式化类SimpleDateFormate是非线程安全的,我们通过ThreadLocal来设置则规避了这一点:

public static final ThreadLocal<DateFormat> df_yyyy_MM_dd = 
			ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

特别注意

当ThreadLocal与线程池一起使用时,便会有一个问题,当从线程池取出一个线程后并归还后,下次从线程池中取同样的线程,保存在ThreadLocal中的值是否会泄露?

不谈实验,根据我们查找的资料,对这个问题有两种看法,一种说法是ThreadLocal中使用的键值并非是Thread,而是线程Thread的弱引用,当线程回收时会触发垃圾回收机制,并不会造成数据泄露;更多的资料反应存在内存泄露问题以及生产环境发生过的惨案,如记一次Java线程池与ThreadLocal引发的血案以及An Introduction to ThreadLocal in Java

更准确地说,当ThreadLocal的修饰符有static时,即强引用的ThreadLocal需要我们手动使用remove方法来释放数据,为了养成良好的习惯,建议还是当ThreadLocal使用结束后,就调用其remove方法。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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