简单理解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的场景最好满足两个条件,一是该对象不需要在多线程之间共享;二是该对象需要在线程内被传递。

完。

参考文章:

正确理解ThreadLocal
一个故事讲明白 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);
}
}
————————————————
版权声明:本文为CSDN博主「qq_36632687」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36632687/java/article/details/79551828

posted @ 2020-04-20 16:55  Lost blog  阅读(675)  评论(1编辑  收藏  举报