ThreadLocal


フルコード:https://github.com/evelyn82ny/ThreadLocal
ThreadLocalは、同時性の問題を解決するクラスであり、各スレッドに独立した空間を提供します.各スレッドは、自分に割り当てられた内部リポジトリでget()、set()クエリー、および値を使用できます.
concurrency issues
Spring beanが単一インスタンスであることを確認します.これは、オブジェクトインスタンスがアプリケーションに1つしか存在しないことを意味します.メモリを節約する利点がありますが、複数のスレッドが同時に1つのフィールドにアクセスすると、同時性の問題が発生するという欠点があります.
同じ商品について、複数の管理者が価格を修正する過程で発生した同期性の問題は、次のとおりです.
  • 提出:8c758342
  • @Slf4j
    public class ItemService {
    
        private Long price;
    
        public void changePrice(Long price)  {
            log.info("저장: 기존 가격 = {} -> 변경 가격= {}", this.price, price);
            this.price = price;
    
            sleep(1000);
            log.info("조회: 현재 가격 = {}", this.price);
        }
    }
    複数の管理者が単一のItemServiceにアクセスします.changePriceメソッドを使用して価格を変更し、1秒後に変更後の価格を問い合わせるときに発生する同期性の問題は次のとおりです.
    同期の問題が発生
    同じ商品の場合、useraは1000元に変更され、userbは2000元に変更されます.useraの価格変更はthread Aで処理し,userbの価格変更はthread Bで処理する.
    void concurrencyTest() {
    	Runnable userA = () -> itemService.changePrice(1000L);
        Runnable userB = () -> itemService.changePrice(2000L);
    
        Thread threadA = new Thread(userA);
        threadA.setName("thread_A");
    
    	Thread threadB = new Thread(userB);
        threadB.setName("thread_B");
    
        threadA.start();
        sleep(100);  
        threadB.start();
    
        sleep(3000);
    }
    2つのスレッド間でsleep(100);に設定されているため、thread Aは開始し、0.1秒後にthread Bは開始する.すなわち、changePrice methodは価格を変更し、1秒後にクエリーを行うため、thread Aがすべての変更およびクエリープロセスを処理する前にthread Bがこの操作を実行する.
    [thread_A] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 1000
    [thread_B] INFO ... 저장: 기존 가격 = 1000 -> 변경 가격 = 2000
    [thread_A] INFO ... 조회: 현재 가격 = 2000
    [thread_B] INFO ... 조회: 현재 가격 = 2000
    thread Aは、実行時nullの価格を1000元に変更します.thread Aが実行され、0.1秒後にthread Bが実行されるため、threadAが変更後の価格を照会する前に、threaddBの価格変更が最初に処理される.このため、上記のログから、thread Aを保存した後、thread Bの保存が実行されていることがわかる.
    thread Bがpricefieldに近づくと、thread Aはそれを1000ウォンに修正し、既存価格は1000ウォンに出力する.
    thread Aはpriceを1000ウォンに修正し、1秒後にpriceを調べたが、出力は1000ウォンではなく2000ウォンだった.1秒の間、thread Bは同じ価格ドメインに近いため、2000ウォンに変更された.

    このように複数のスレッドが同じ参照にアクセスして変更する場合に発生する問題を同期性問題と呼ぶ.
    threadA.start();
    threadB.start();
    上のsleep(100);コードを削除すると、次のログが出力されます.
    [thread_A] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 1000
    [thread_B] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 2000
    [thread_A] INFO ... 조회: 현재 가격 = 2000
    [thread_B] INFO ... 조회: 현재 가격 = 2000
    2つのスレッドはほぼ同時に同じフィールドにアクセスするため、両方が空の値で出力され、クエリ時に最後に変更された価格出力は2000元であることがわかります.
    同期に問題はありません
    void concurrencyTest() {
    	Runnable userA = () -> itemService.changePrice(1000L);
        Runnable userB = () -> itemService.changePrice(2000L);
    
        Thread threadA = new Thread(userA);
        threadA.setName("thread_A");
    
    	Thread threadB = new Thread(userB);
        threadB.setName("thread_B");
    
        threadA.start();
        sleep(2000);  
        threadB.start();
    
        sleep(3000);
    }
    同期の問題を回避するために、sleep()設定時間を2つのthread間で2秒に設定します.すなわち、threadaは、処理が完了してから2秒後にthreadbを処理する.
    [thread_A] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 1000
    [thread_A] INFO ... 조회: 현재 가격 = 1000
    [thread_B] INFO ... 저장: 기존 가격 = 1000 -> 변경 가격 = 2000
    [thread_B] INFO ... 조회: 현재 가격 = 2000
    上記のログでは、thread Aが変更およびクエリーを行い、thread Bの保存およびクエリーを実行します.つまり,同じ領域に近いが,相互に影響を及ぼさない.
    2つのスレッドを調整するだけで済むので,sleep()は同期性の問題を解決できる.ただし,実際のサービスで使用される大量のスレッドについては,sleep()を1つずつ設定することはできない.ThreadLocalを使用してこの問題を解決します.
    Apply ThreadLocal

    ThreadLocalは、Thread単位でローカル変数を割り当てます.すなわち、各スレッドには個別の内部ストレージがあるため、同じインスタンスのthread localフィールドにアクセスしても同期の問題は発生しません.
    public class ItemService {
    
        // private Long price;
        private ThreadLocal<Long> price = new ThreadLocal<>();
    }
    pricefieldの場合、上記のLongタイプは、スレッドごとに個別の内部ストレージを提供するためにThreadLocal<Long>に変更される.各threadは、次の方法でフィールドに値を格納、クエリー、削除します.
  • :ThreadLocalを保存します.set(value);
  • クエリー:ThreadLocal.get();
  • :ThreadLocalを削除します.remove();
  • 各スレッドには独立した内部ストレージがあるため、インデックスアクセスではなくset(value)とget()を作成するだけです.
  • 提出:ff25ff32
  • public class ItemService {
    
        private String name;
        // private Long price;
        private ThreadLocal<Long> price = new ThreadLocal<>();
        
        public void changePrice(Long price)  {
        	// this.price -> this.price.get() 으로 변경
            log.info("저장: 기존 가격 = {} -> 변경 가격 = {}", this.price.get(), price);
            
            // this.price = price 이 아닌 set() 사용
            this.price.set(price);
    
            sleep(1000);
    
            log.info("조회: 현재 가격 = {}", this.price.get());
        }
    }
    threadA.start();
    sleep(2000);
    threadB.start();
    2つのスレッドの間にsleep(2000);が設定されています.
    [thread_A] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 1000
    [thread_A] INFO ... 조회: 현재 가격 = 1000
    [thread_B] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 2000
    [thread_B] INFO ... 조회: 현재 가격 = 2000
    sleep(2000);、thread Aでクエリーを行った場合、weblogでthread Bの保存が完了したことを確認できます.
    理論的には同じフィールドに近いが、ThreadLocalが適用されているためthread Aが完了し、thread Bアクセスpriceフィールドもpriceの初期値nullを出力する.これは、各スレッドに内部リポジトリがあるためです.
    threadA.start();
    threadB.start();
    [thread_A] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 1000
    [thread_B] INFO ... 저장: 기존 가격 = null -> 변경 가격 = 2000
    [thread_A] INFO ... 조회: 현재 가격 = 1000
    [thread_B] INFO ... 조회: 현재 가격 = 2000
    2つのスレッドがpricefieldを同時に変更しても、各スレッドは変更後の価格でクエリーされ、スレッド間に影響がないことを示します.
    Thread Pool
    アプリケーション開発にスレッドが必要な場合は、作成して使用できます.スレッドを生成するたびに、オペレーティングシステムはスタック領域を取得し、使用しないとメモリ領域を回収する操作に大きなコストがかかります.

    この問題を解決するために、Thread Poolで複数のスレッドを事前に作成し、タスクを割り当てます.作業が完了すると、スレッドは削除されずに再使用され、コスト削減が回避されます.
    ThreadLocal.remove()
    Thread Poolのスレッドが再使用されます.作業が完了して戻ってきたスレッドには、データがまだ存在します.返却によって自動的に削除されることはありません.
    したがって、他のタスクを対応するスレッドに割り当てると、不要なデータにアクセスできます.重要なデータの場合、深刻なデータ漏洩の問題が発生します.したがって、完了後、remove()を呼び出してスレッド内のローカルデータを削除する必要があります.
  • 提出:9953d742
  • log.info("조회: 현재 가격 = {}", this.price.get());
    
    this.price.remove();
    log.info("thread 삭제: 가격 = {}", this.price.get());
    [thread_A] INFO ... 조회: 현재 가격 = 1000
    [thread_A] INFO ... thread 삭제: 가격 = null