ThreadLocalの原理と適用シーンを簡単に理解し、マルチデータソースの下でThreadLocalの応用

8576 ワード

一、ThreadLocal簡単な紹介


まず,ThreadLocalは本スレッドの変数を維持するためのものであり,共有変数の同時問題を解決することはできない.ThreadLocalは、各スレッドがそのスレッドのmapに値を格納し、ThreadLocal自身をkeyとし、必要に応じてそのスレッドの前に格納した値を得る.共有変数が格納されている場合、取り出したのも共有変数であり、同時問題は存在します.
例を簡単に見てみましょう.
public class TestThreadLocal {
    private static final ThreadLocal threadLocalA = new ThreadLocal<>();
    private static final ThreadLocal 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の主な用途は、スレッド自体のオブジェクトを保持し、パラメータの伝達を回避することです.主な適用シーンは、スレッドによるマルチインスタンス(スレッドごとに1つのインスタンスに対応)のオブジェクトへのアクセスであり、このオブジェクトは多くの場所で使用されます.
最初の例:データベース接続
private static ThreadLocal connectionHolder = new ThreadLocal() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  
第2例:データソースを動的に設定する
まずその実現を簡単に紹介します
最初のステップでは、プロジェクトの開始時に構成で設定された複数のデータソースをロードし、カスタム名または他のフラグをkeyとします.
第2のステップでは、フレームワーク内のAbstractRoutingDataSourceクラスがkeyを提供する方法を実装し、フレームワークソースコードはデータベースにアクセスするたびにこの方法を呼び出してデータソースのkeyを取得し、keyを通じて特定のデータソースを取得します.
第3のステップでは、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 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 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を静的変数として使用すると、同時にデータベースにアクセスすると、1つのスレッドが設定したばかりのkeyが別のスレッドによって変更され、最終的にアクセスするデータソースが正しくないという深刻な問題が発見されます.では、keyが他のスレッドに修正されないことを保証するにはどうすればいいのでしょうか.つまり、同時制御もできないし、各スレッドがDynamicData Sourceをインスタンス化してスレッドのkeyを設定することもできないので、ThreadLocalはスレッドのプライベート変数を保護するのに役立ちます.
DynamicDataSourceKeyクラスを変更するには、次の手順に従います.
public class DynamicDataSourceKey {
    private static final ThreadLocal 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によってこのスレッドでデータソースにアクセスするキーを設定することで、オブジェクトを保護するのに役立ちます.まとめると、個人的にはThreadLocalを使用するシーンは、マルチスレッド間で共有する必要がない2つの条件を満たすことが望ましいと考えられています.2つ目は、オブジェクトがスレッド内で伝達される必要があることです.
終わります.
参考記事:
  • ThreadLocal
  • を正しく理解する
  • ストーリーがわかる
    ps:動的データソースを実現するための後続のステップである第3のステップ(人人開源プロジェクトを参照).
    複数データ・ソースの注釈と名前クラスをカスタマイズするには、次の手順に従います.
    @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);
        }
    }