简单理解ThreadLocal原理和适用场景,多数据源下ThreadLocal的应用

一、ThreadLocal简单介绍

首先,ThreadLocal是用来维护本线程的变量的,并不能解决共享变量的并发问题。ThreadLocal是各线程将值存入该线程的map中,以ThreadLocal自身作为key,需要用时获得的是该线程之前存入的值。如果存入的是共享变量,那取出的也是共享变量,并发问题还是存在的。

简单看一下例子:

public class TestThreadLocal {
    private static final ThreadLocal<String> threadLocalA = new ThreadLocal<>();
    private static final ThreadLocal<String> threadLocalB = new ThreadLocal<>();

    /**
     * 在调用的线程的map中存入key为ThreadLocal本身,value为在该线程设置的值
     * @param value
     */
    public static void setValueA(String value){
        threadLocalA.set(value);
    }

    public static String getValueA(){
        return threadLocalA.get();
    }

    public static void clearValueA(){
        threadLocalA.remove();
    }

    public static void setValueB(String value){
        threadLocalB.set(value);
    }

    public static String getValueB(){
        return threadLocalB.get();
    }

    public static void clearValueB(){
        threadLocalB.remove();
    }


    public static void main(String[] args) {
        //线程1的ThreadLocalMap中存着key为threadLocalA,value为A1;key为threadLocalB,value为B1
        new Thread(){
            @Override
            public void run(){
                setValueA("A1");
                System.out.println("thread1:" + getValueA());
                clearValueA();

                setValueB("B1");
                System.out.println("thread1:" + getValueB());
                clearValueB();
            }
        }.start();

        //线程2的ThreadLocalMap中存着key为threadLocalA,value为A2;key为threadLocalB,value为B2
        new Thread(){
            @Override
            public void run(){
                setValueA("A2");
                System.out.println("thread2:" + getValueA());
                clearValueA();

                setValueB("B2");
                System.out.println("thread2:" + getValueB());
                clearValueB();
            }
        }.start();
    }
}

该例子的执行结果为:

thread2:A2
thread2:B2
thread1:A1
thread1:B1

从该例子可以看出多个线程设置ThreadLocal的值只在该线程的作用范围内有效。操作ThreadLocal的set,get方法实际上是操作该线程的一个代理,其本质是在该线程的ThreadLocalMap中存储了key为ThreadLocal本身和对应的值。

以下是ThreadLocal中的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);
    }

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

二、ThreadLocal使用场景举例

ThreadLocal既然不能解决并发问题,那么它适用的场景是什么呢?

ThreadLocal的主要用途是为了保持线程自身对象和避免参数传递,主要适用场景是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

第一个例子:数据库连接

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  
第二个例子:动态设置数据源

首先简单介绍一下它的实现

第一步,在项目启动时加载配置中设置的多种数据源,以自定义的名字或其他标志作为key。

第二步,继承框架中的 AbstractRoutingDataSource类实现提供key的方法,框架源码会在每次访问数据库时都会调用这个方法获得数据源的key,再通过key获得具体数据源。

第三步,通过AOP和注解拦截访问数据库的方法,在访问前设置该方法调用的key变量。

那么我们主要关注第二步怎么在访问数据源前获得key,即实现提供key的方法。

以下是多数据源的配置,也可以通过spring的xml配置:

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<String, DataSource> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

以下是实现提供key的方法(我们需要关注的):

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<String, DataSource> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(new HashMap<>(targetDataSources));
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        //确定需要使用的数据源的key
    }

}

现在让我们开始想怎么实现该方法。首先需要定义一个关于key的获取、修改的类:

public class DynamicDataSourceKey {
    private static String key;

    public static String getDataSourceKey(){
        return key;
    }

    public static void setDataSource(String dataSourceKey) {
        key = dataSourceKey;
    }

}

determineCurrentLookupKey方法通过getDataSourceKey方法得到key

@Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceKey.getDataSourceKey();
    }

这里可以发现一个很严重的问题,如果将key作为静态变量那可能引起并发问题,当同时访问数据库时,一个线程刚刚设置的key可能被另一个线程修改了,导致最终访问的数据源不正确。那么怎么样才能保证key能不被其它线程修改呢,即不能控制并发也不能每个线程都实例化DynamicDataSource来设置该线程的key,这时候ThreadLocal就能起到很好的作用,保护该线程私有的变量。

修改DynamicDataSourceKey类:

public class DynamicDataSourceKey {
    private static final ThreadLocal<String> key = new ThreadLocal<>();

    public static String getDataSourceKey(){
        return key.get();
    }

    public static void setDataSource(String dataSourceKey) {
        key.set(dataSourceKey);
    }

    public static void clearDataSource() {
        key.remove();
    }
}

通过ThreadLocal设置该线程中访问数据源的key,起到很好的保护对象的作用。总结一下,个人认为使用ThreadLocal的场景最好满足两个条件,一是该对象不需要在多线程之间共享;二是该对象需要在线程内被传递。

完。

参考文章:

ps:补充实现动态数据源的后续步骤即第三步(参考人人开源项目)。

自定义多数据源注解和名称类:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}
public interface DataSourceNames {
    String FIRST = "first";
    String SECOND = "second";
}

切面处理类:

@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(io.renren.datasources.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        DataSource ds = method.getAnnotation(DataSource.class);
        if(ds == null){
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.debug("set datasource is " + DataSourceNames.FIRST);
        }else {
            DynamicDataSource.setDataSource(ds.name());
            logger.debug("set datasource is " + ds.name());
        }

        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.debug("clean datasource");
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

使用类:

@Service
public class DataSourceTestService {
    @Autowired
    private UserService userService;

    public UserEntity queryObject(Long userId){
        return userService.queryObject(userId);
    }

    @DataSource(name = DataSourceNames.SECOND)
    public UserEntity queryObject2(Long userId){
        return userService.queryObject(userId);
    }
}

  • 5
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值