java多线程学习之【ThreadLocal】

目前卷文化盛行,为了增强面试能力,开始了无边无际的学习,无边界不是重点,重点是要深入1万米。言归正传,本文打算做一个多线程学习的系列文章,沉淀自我。


前言

本文主要是讲解ThreadLocal的概念,基本用法,使用场景,底层代码原理剖析。

一、ThreadLocal是什么?

ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

按照源码注释,翻译成中文,就是以下内容:

  • 这个类提供了线程局部变量。这些变量不同于正常的线程中的变量,每一个线程访问一个(通过get set方法)都有自己的独立的初始化变量的副本。线程本地实例通常是私有静态字段,希望把状态与线程关联(例如,用户ID或事务ID)。
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

只要线程还活着和线程本地实例可访问,每一个线程持有一个隐式引用线程局部变量的副本(ThreadLocalMap);线程消失后,所有的线程本地副本实例受到垃圾收集(除非存在这些副本的其他引用)。

package com.valley.jdk.threadlocal;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author valley
 * @date 2022/7/20
 * @Description TODO
 */
public class ThreadId {

    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return nextId.getAndIncrement();
        }
    };

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }

    public static void main(String[] args) throws InterruptedException {
        List<Thread> tds=new ArrayList<>();
        for(int i=0;i<10;i++){
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("id="+get());
                }
            });
            tds.add(thread);
        }

        for(Thread td:tds){
            td.start();
        }

        for(Thread td:tds){
            td.join();
        }
    }

}

二、使用方法

ThreadLocal 提供了一些方法:

方法说明
createMap(Thread t, T firstValue)创建线程相关的线程本地,注意这个Map的key是线程本地对象,而不是线程对象
get()返回ThreadLocal变量在这个当前线程中副本的值。
getMap(Thread t)返回线程的ThreadLocalMap。
initialValue()初始化重写的方法。
remove()删除当前线程的ThreadLocal变量
set(T value)设置当前线程这个ThreadLocal变量设置为指定的值
setInitialValue()设初始化set

三、应用场景

ThreadLocal使用在动态数据源应用上,特别是涉及到很多的数据库时,一定会用到。

3.1动态多源数据库

定义线程本地对象

  • 代码如下:
public class DataSourceContextHolder {
	private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static synchronized void setDBType(String dbType){
        contextHolder.set(dbType);
    }

    public static String getDBType(){
        return contextHolder.get();
    }

    public static void clearDBType(){
        contextHolder.remove();
    }
}

定义动态数据源类,继承AbstractRoutingDataSource类

  • 代码如下:
import java.util.HashMap;
import java.util.Map;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 
 * Created by zhangyong on 2018年11月15日
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
	private static DynamicDataSource instance;
    private static byte[] lock=new byte[0];
    private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
    }

    public Map<Object, Object> getDataSourceMap() {
        return dataSourceMap;
    }

    public static synchronized DynamicDataSource getInstance(){
        if(instance==null){
            synchronized (lock){
                if(instance==null){
                    instance=new DynamicDataSource();
                }
            }
        }
        return instance;
    }
    //必须实现其方法
    @Override
    protected Object determineCurrentLookupKey() {
        String string = DataSourceContextHolder.getDBType();
        if (string != null){
            return string;
        }else return "default";

    }

}

默认数据源配置DataSourceConfig

  • 代码如下:
@Configuration
public class DataSourceConfig {
	 	@Value("${spring.datasource.default.url}")
	    private String defaultDBUrl;
	    @Value("${spring.datasource.default.username}")
	    private String defaultDBUser;
	    @Value("${spring.datasource.default.password}")
	    private String defaultDBPassword;
	    @Value("${spring.datasource.default.driverClassName}")
	    private String defaultDBDreiverName;

	    @Bean
	    public DynamicDataSource dynamicDataSource() {
	        DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();

	        DruidDataSource defaultDataSource = new DruidDataSource();
	        defaultDataSource.setUrl(defaultDBUrl);
	        defaultDataSource.setUsername(defaultDBUser);
	        defaultDataSource.setPassword(defaultDBPassword);
	        defaultDataSource.setDriverClassName(defaultDBDreiverName);


	        Map<Object,Object> map = new HashMap<>();
	        map.put("default", defaultDataSource);
	        dynamicDataSource.setTargetDataSources(map);
	        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);

	        return dynamicDataSource;
	    }

	    @Bean
	    public SqlSessionFactory sqlSessionFactory(
	            @Qualifier("dynamicDataSource") DataSource dynamicDataSource)
	            throws Exception {
	        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
	        bean.setDataSource(dynamicDataSource);
	        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*/*.xml"));
	        bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:config/mybatis-config.xml"));
	        bean.setTypeAliasesPackage("com.valley.qm.system.domain");
	        return bean.getObject();

	    }

	    @Bean(name = "sqlSessionTemplate")
	    public SqlSessionTemplate sqlSessionTemplate(
	            @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
	            throws Exception {
	        return new SqlSessionTemplate(sqlSessionFactory);
	    }
}

应用程序初始化加载数据源列表。

  • 代码如下:
@SpringBootApplication
@EnableAsync //开启异步调用
@EnableTransactionManagement
public class Application implements CommandLineRunner{

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Autowired
    ITdatasourcesRulesService rulesService;

    @Override
    public void run(String... strings) throws Exception {

        /**
         * 获取default数据库信息
         */

        DataSourceContextHolder.setDBType("default");

        List<String> list = rulesService.selectId();
        if (list.size() > 0){
            for (String datasourceId:list) {
                TdatasourcesRules rules = rulesService.selectByKey(datasourceId);
                if (rules != null || !rules.equals("")){
                    String url = "jdbc:mysql://" + rules.getIp() + ":" + rules.getPort() + "/" + rules.getDatabaseName() + "?useUnicode=true&characterEncoding=utf8";

                    DruidDataSource dynamicDataSource = new DruidDataSource();
                    dynamicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
                    dynamicDataSource.setUrl(url);
                    dynamicDataSource.setUsername(rules.getUsername());
                    dynamicDataSource.setPassword(rules.getPassword());

                    /**
                     * 创建动态数据源
                     */
                    Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
                    dataSourceMap.put(datasourceId, dynamicDataSource);
                    DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
                }
            }
        }
    }
}

具体使用切换数据源方法

  • 代码如下:
    public void doEsAndDataTask(String dbType){

        //切换动态数据源
        DataSourceContextHolder.setDBType(dbType);
        List<MacDidMsgDto> list = didMsgService.queryPart();

		//清除线程本地变量的副本,一般数据库链接都会用到线程池,所以要清除
        DataSourceContextHolder.clearDBType();
       
    }

切换数据源,最好是采用Aop+自定义注解,在需要切换数据源的方法上添加此注解,利用编写的自定义注解的解析器获取注解中配置的目标数据源,从而进行动态数据源切换。

重点:除了每个线程维护一个ThreadLocalMap,我们这里也需要维护一个数据源对象Map,数据源对象Map的key是我们的每个数据库ID,ThreadLocalMap的值是数据库ID。具体两者的映射逻辑交给了Spring-jdbc来处理的。
在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收,避免内存泄漏。
所以setDBType和clearDBType是成对出现。

四、源码剖析

ThreadLocal的初始化可以匿名内部类重写initialValue方法,也可以直接定义。

	private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

我们通过获取本地变量,来剖析下源码

  • 获取ThreadLocalMap,然后通过ThreadLocal对象this作为Key获取值返回
    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();
    }
  • getMap的参数是当前线程,返回了Map,类似透传的方法。从这里可以看出每个线程都有一个ThreadLocalMap。ThreadLocalMap是在ThreadLocal定义的一个静态内部类。
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • 设置初始化值,可以更清楚地上面的结论。
   private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

五、和其他工具比较

  • ThreadLocal与InheritableThreadLocal区别

InheritableThreadLocal继承ThreadLocal,InheritableThreadLocal使用优先于ThreadLocal的场景是,被保存在变量的每个线程的属性(例如,用户ID、事务ID)必须自动传送到创建的任何子线程。

  • ThreadLocal与Thread,ThreadLocalMap之间的关系

ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap这个属性值的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的变量。
在这里插入图片描述

  • ThreadLocal与Synchronized的区别

ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别:

  • Synchronized用于线程间的数据共享,而ThreadLocal用于线程间的数据隔离。
  • Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

blackoon88

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值