Java线程之ThreadLocal解析

 更新时间:2023年09月26日 09:13:10   作者:奋斗的小面包  
这篇文章主要介绍了Java线程之ThreadLocal解析,ThreadLocal 提供线程的局部变量,每个线程都可以通过get()和set()对局部变量进行操作而不会对其他线程的局部变量产生影响,实现了线程之间的数据隔离,需要的朋友可以参考下

ThreadLocal实现原理

ThreadLocal.ThreadLocalMap

首先,每个Thread 里面都有一个成员 ThreadLocal.ThreadLocalMap 类型的成员变量

    static class ThreadLocalMap {
        //内部存储其实是一个 entry 的数组结构
        private Entry[] table;
    }
  static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

看到这里我们应该清楚了 ThreadLocal.ThreadLocalMap 的数据结构,如下图

因为ThreadLocal.ThreadLocalMap 类型的变量 是Thread 的成员变量,所以其有线程隔离性.

那么ThreadLocal.ThreadLocalMap中的数据是从什么地方写入或者读取的呢?那时就ThreadLocal这个类所实现的功能了。

ThreadLocal

这里我们着重分析一个 ThreadLocal的 get() 方法set(T value)方法remove()方法

 public T get() {
     //获取当前线程
        Thread t = Thread.currentThread();
     //获取当前线程的threadLocals 成员变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //获取以当前TheadLocal对象为key 的key-value 键值对
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //返回value
                T result = (T)e.value;
                return result;
            }
        }
     //否则调用setInitialValue方法返回默认值
        return setInitialValue();    1
    }
1    
private T setInitialValue() {
        //我们可以重载该方法,初始化默认值
        T value = initialValue();    2
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //创建ThreadLocalMap,并存储到Thread 中
            createMap(t, value);   3
        return value;
    }
//我们可以重载该方法,初始化默认值
2
  protected T initialValue() {
        return null;
    }
//创建
3
void createMap(Thread t, T firstValue) {
    //赋值给当前线程
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

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);
    }

remove()方法

   public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

threadLocal 使用风险

最大的风险就是产生的内存泄漏风险,

 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

通过构造函数我们知道 key 为弱引用,value 为强引用。

当threadLocal变量被置为null时,Heap中的threadLocal对象失去了引用,将被GC回收。同时Entry的key也将被回收。Entry中只剩下没有key的Value,此时存在强引用链threadlocalmap–>Entry–>Value,若当前线程迟迟不能结束,则Heap中的Value始终不能被GC回收,造成内存泄漏。所以必须建议 最好的办法在不使用ThreadLocal的时候,调用remove()方法,通过显示 的设置value = null 清除数据。

为了避免内存泄漏,ThreadLocalMap在调用get()方法和set()方法时操作数组时,也会去调用expungeStaleEntry()方法来清除Entry中key为null的Value,但是这种清理是不及时,因为我们不保证时候还会触发get()方法和set()等方法。因此也会引发内存泄漏的风险。只有remove()方法,显式调用expungeStaleEntry()方法,才是王道。

使用场景

场景1

线程内保存全局变量,可以让不同方法直接使用,避免参数传递麻烦,例如数据源切换

@Configuration
public class DataSourceProxyConfig {
	//数据源1
    @Bean("originOrder")
    @ConfigurationProperties(prefix = "spring.datasource.order")
    public DataSource dataSourceMaster() {
        return new DruidDataSource();
    }
	//数据源2
    @Bean("originStorage")
    @ConfigurationProperties(prefix = "spring.datasource.storage")
    public DataSource dataSourceStorage() {
        return new DruidDataSource();
    }
	//数据源3
    @Bean("originPay")
    @ConfigurationProperties(prefix = "spring.datasource.pay")
    public DataSource dataSourcePay() {
        return new DruidDataSource();
    }
	//数据源4
    @Bean(name = "order")
    public DataSourceProxy masterDataSourceProxy(@Qualifier("originOrder") DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
	//数据源5
    @Bean(name = "storage")
    public DataSourceProxy storageDataSourceProxy(@Qualifier("originStorage") DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
	//数据源6
    @Bean(name = "pay")
    public DataSourceProxy payDataSourceProxy(@Qualifier("originPay") DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("order") DataSource dataSourceOrder,
                                        @Qualifier("storage") DataSource dataSourceStorage,
                                        @Qualifier("pay") DataSource dataSourcePay) {
		//动态数据源,这是spring 为我们提供的
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(3);
        dataSourceMap.put(DataSourceKey.ORDER.name(), dataSourceOrder);
        dataSourceMap.put(DataSourceKey.STORAGE.name(), dataSourceStorage);
        dataSourceMap.put(DataSourceKey.PAY.name(), dataSourcePay);
        dynamicRoutingDataSource.setDefaultTargetDataSource(dataSourceOrder);
        //数据源以键值对的形式存储
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
        DynamicDataSourceContextHolder.getDataSourceKeys().addAll(dataSourceMap.keySet());
        return dynamicRoutingDataSource;
    }
    @Bean
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dynamicDataSource") DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }
}
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        log.info("当前数据源 [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
        //调用我们的ThreadLocal 获取数据源的key
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = ThreadLocal.withInitial(DataSourceKey.ORDER::name);
    private static List<Object> dataSourceKeys = new ArrayList<>();
    public static void setDataSourceKey(DataSourceKey key) {
        CONTEXT_HOLDER.set(key.name());
    }
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
    public static List<Object> getDataSourceKeys() {
        return dataSourceKeys;
    }
}

在程序代码中我们就可以使用DynamicDataSourceContextHolder.setDataSourceKey(),进行数据源的切换了。

在业务代码执行完成后,记得显示调用clearDataSourceKey()方法清除数据。

为了方便使用,我们完成一下,就是可以在需要切换数据源 Service 或 Mapper 方法上添加 @DataSource 注解,来实现数据源的切换功能

本实现出自ruoyi项目,感谢若依

声明一个切面,拦截包含 @DataSource注解的方法

@Component
public class DataSourceAspect
{
    protected Logger logger = LoggerFactory.getLogger(getClass());
    @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
            + "|| @within(com.ruoyi.common.annotation.DataSource)")
    public void dsPointCut()
    {
    }
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable
    {
        DataSource dataSource = getDataSource(point);
        if (StringUtils.isNotNull(dataSource))
        {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try
        {
            return point.proceed();
        }
        finally
        {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point)
    {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource))
        {
            return dataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    /**
     * 切换数据源名称
     */
    public DataSourceType value() default DataSourceType.MASTER;
}

这样就可以在方法上直接使用@DataSource 注解实现数据源的切换功能了。再次强调,一定要显示调用remove 方法确保内存回收。

场景2

每个线程需要一个独享的对象,比如非线程安全的工具类 例如SimpleDateFormt

class SimpleDateFormtHolder {
    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyy-MM-dd");
        }
    };
    public static SimpleDateFormat getSimpleDateFormt() {
        return threadLocal.get();
    }
    public static void setSimpleDateFormt(SimpleDateFormat simpleDateFormat) {
         threadLocal.set(simpleDateFormat);
    }
    public static void removeSimpleDateFormt() {
        threadLocal.remove();
    }
}

到此这篇关于Java线程之ThreadLocal解析的文章就介绍到这了,更多相关ThreadLocal解析内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决Spring Mvc中对象绑定参数重名的问题

    解决Spring Mvc中对象绑定参数重名的问题

    最近在工作中遇到了参数绑定的一个问题,发现网上这方面的资料较少,索性自己来总结下,下面这篇文章主要给大家介绍了关于如何解决Spring Mvc中对象绑定参数重名问题的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-08-08
  • Java注释代码执行方法解析

    Java注释代码执行方法解析

    这篇文章主要介绍了Java注释代码执行方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05
  • 关于Spring Boot项目的 log4j2 核弹漏洞问题(一行代码配置搞定)

    关于Spring Boot项目的 log4j2 核弹漏洞问题(一行代码配置搞定)

    相信昨天,很多小伙伴都因为Log4j2的史诗级漏洞忙翻了吧,不过我看到群里发出来的各种修复方法,还真是不好看...所以这里也提一下Spring Boot用户怎么修复最简单吧,对Spring Boot log4j2 核弹漏洞问题感兴趣的朋友参考下吧
    2021-12-12
  • SpringBoot集成cache缓存的实现

    SpringBoot集成cache缓存的实现

    日常开发中,缓存是解决数据库压力的一种方案,本文记录springboot中使用cache缓存。需要的朋友们下面随着小编来一起学习学习吧
    2021-06-06
  • Redis工具类封装RedisUtils的使用示例

    Redis工具类封装RedisUtils的使用示例

    本文主要介绍了Redis工具类封装RedisUtils的使用示例,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • java中struts 框架的实现

    java中struts 框架的实现

    本文给大家介绍的是java中struts 框架的实现,有需要的小伙伴可以参考下。
    2015-06-06
  • mybatis-plus 如何判断参数是否为空并作为查询条件

    mybatis-plus 如何判断参数是否为空并作为查询条件

    这篇文章主要介绍了mybatis-plus 如何判断参数是否为空并作为查询条件,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • SpringBoot上传临时文件被删除引起报错的解决

    SpringBoot上传临时文件被删除引起报错的解决

    这篇文章主要介绍了SpringBoot上传临时文件被删除引起报错的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Spring基于注解的缓存声明深入探究

    Spring基于注解的缓存声明深入探究

    spring boot对缓存支持非常灵活,我们可以使用默认的EhCache,也可以整合第三方的框架,只需配置即可,下面这篇文章主要给大家介绍了关于SpringBoot学习之基于注解缓存的相关资料,需要的朋友可以参考下
    2022-08-08
  • java实现Excel转换为图片

    java实现Excel转换为图片

    在实际开发过程中,经常会有这样的需求,需要将Excel表格或特定区域转换为图片,所以小编今天就来为大家介绍一下如何使用Java将Excel转化为图片吧
    2023-10-10

最新评论