주요 콘텐츠로 건너뛰기

CooLWJ 검색

选择搜索范围,输入关键词后按回车。

언어

Java 스레드 컨텍스트

ThreadLocal은 JDK의 java.lang 패키지에 속한 자주 쓰이는 유틸리티 클래스로, 스레드 컨텍스트 기능을 구현한다. 가장 흔한 활용 사례는 아마도 로그인한 사용자의 컨텍스트 같은 핵심 정보를 ThreadLocal에 저장하는 것일 것이다.

스레드 컨텍스트는 복잡해 보이는 여러 문제도 깔끔하게 해결할 수 있다. 예를 들면 spring-mybatis 프레임워크 통합 문제가 있다:
spring 생태계를 기반으로 개발할 때는 일반적으로 트랜잭션 관리를 spring에 위임하고, transactional 애너테이션으로 트랜잭션 시작 여부와 격리 수준 등의 정보를 설정합니다. 데이터베이스도 jdbc를 직접 사용하지 않고 mybatis 프레임워크를 통해 접근합니다. spring과 mybatis는 완전히 독립적인 두 프레임워크인데도, 사용할 때는 spring이 연 데이터베이스 connection을 mybatis가 바로 가져다 쓸 수 있는 것처럼 보입니다. 우리는 사실상 아무것도 할 필요 없이 mybatis-spring-boot-stater jar 패키지만 추가하면 됩니다.
이 우아한 설계를 구현하는 핵심은 ThreadLocal이다. 대략적인 원리는 stater jar 패키지가 최종적으로 하나의 것을 도입해 준다는 것이다 MybatisAutoConfiguration그리고 SPI를 통해 우리 서비스의 bean 컨테이너에 주입하고,MybatisAutoConfiguration에 설정해 두었다mybatis필요한SqlSessionFactory,그리고 spring 자체의 것을 주입했다 SpringManagedTransactionFactory(구현한TransactionFactory,트랜잭션 팩토리는 mybatis가 제공하는 확장 메커니즘으로, 트랜잭션 관리를 외부에 위임할 수 있습니다).
전체 실행 흐름은 transactional 애너테이션이 붙은 메서드가 최종적으로 AOP 프록시의 대상이 되고, 프록시 클래스가 사전 어드바이스에서 dataSource로부터 connection을 얻어 최종적으로 ThreadLocal에 넣는 것이다(실제로 ThreadLocal에 들어가는 것은 map이며, key는 dataSource이고 value는 connection에 해당하는 connectionHolder이다). 이후 mybatis가 sql을 실행할 때는 먼저 다음을 통해TransactionFactoryconnection을 가져온다, 이때 주입된TransactionFactory예SpringManagedTransactionFactory,따라서 ThreadLocal에서 해당 dataSource의 connection을 가져온다.

ThreadLocal의 간단한 원리 소개:

tapd_50394669_1660562598_27.png

ThreadLocalMap는 선형 탐사법으로 충돌을 해결하는 해시 테이블입니다. 각 스레드마다 하나씩 생성되며, 각 ThreadLocal 객체 안에는 하나의 threadLocalHashCode해시 키를 계산하는 데 사용한다.
HashMap과 비교해 보면,ThreadLocalMap충돌 해결 알고리즘으로 체이닝 대신 선형 탐사를 선택한 데에는 다음과 같은 고려가 있었을 수 있다:
1、메모리 고려. 사용 환경이 다르기 때문에 ThreadLocalMap의 데이터 양은 일반적으로 HashMap보다 훨씬 적습니다. 데이터 양이 적을 때 체이닝 방식은 포인터로 인한 공간 소비가 훨씬 많이 발생합니다
2、효율성, 1을 바탕으로 원소 수가 적을 때 절약한 공간을 사용해 적재율이 더 낮은 해시 테이블을 만들면, 충돌을 줄이고 평균 검색 속도를 높일 수 있다

ThreadLocal、스레드와 ThreadLocalMap 관계도

문제
A 스레드 풀 기반의 작업 환경에서는 컨텍스트 전달 문제가 존재한다。특히 Tomcat 기반의 웹 프로그래밍 모델에서는,Tomcat 자체가 스레드 풀을 사용해 서버와 클라이언트 사이의 요청-응답 상호작용을 처리하므로,각 웹 애플리케이션이 스레드 컨텍스트를 올바르게 전달하는 문제를 직접 처리해야 한다。
B 효율 문제,애플리케이션에 ThreadLocal이 너무 많아 해시 테이블이 커지면 조회 속도가 떨어집니다

문제 A에 대해서는 일반적으로 쓰는 해결 방식이 대체로 같으므로, 우리 시스템의 코드를 그대로 붙여 보겠다
2.png
이 방식에는 두 가지 문제가 있다:
1、이후 새로 추가된 ThreadLocal도 올바르게 전달하려면,코드를 수정해 wrap 로직에 새 ThreadLocal을 추가해야 합니다. 하나라도 빠뜨리면 버그가 발생합니다
2、일부 상황에서는 작업을 제출한 스레드가 직접 작업을 실행할 수도 있다. 예를 들면:

  • 스레드 풀이 가득 차고 스레드 풀의 RejectedExecutionHandler 사용하는 것은 CallerRunsPolicy 일 때, 스레드 풀에 제출한 작업이 제출한 스레드에서 직접 실행되며, 이때 마지막 정리 로직으로 인해 제출한 스레드의 컨텍스트가 사라진다

  • 마찬가지로,사용하는 것은 ForkJoinPool (병렬 실행 포함 Stream 및 CompletableFuture ,내부에서는 ForkJoinPool )의 상황에서, 펼쳐진 ForkJoinTask 작업을 제출한 스레드에서 직접 실행된다。마지막 정리 역시 제출 스레드의 컨텍스트 손실을 초래한다

해결 방법:
문제1:ThreadLocal 클래스가 애플리케이션에 현재 ThreadLocal 객체가 모두 몇 개 있는지 알아낼 수 있는 API를 지원한다면 쉽게 해결되겠지만, 아쉽게도 지원하지 않습니다. 굳이 구현하려면 리플렉션으로 thread 객체 안의inheritableThreadLocals(즉, 하나의 ThreadLocalMap)속성을 찾은 다음, 해시 테이블을 순회하며 모든 entry를 꺼내면 모든 threadlocal 키 값 쌍을 얻을 수 있습니다. 또 다른 방법은 jdk의 ThreadLocal을 한 겹 더 감싸서 위의 기능을 추가하는 것입니다.
문제 2:wrap의 로직을 개선하면 해결할 수 있습니다:스레드가 실행을 시작할 때 먼저 현재 스레드의 컨텍스트를 가져와 임시로 저장한 다음, 작업을 제출한 스레드의 컨텍스트로 현재 스레드의 컨텍스트를 교체합니다. 스레드 실행이 끝나기 전에는 이를 지우지 않고, 임시 저장한 데이터로 현재 스레드의 컨텍스트를 복원하도록 변경합니다.

transmittable-thread-local
이미 만들어진 도구가 있습니다. 그 구현 원리는 위의 두 방향에서 접근하는 것이며, 이 과정에 이름도 붙였습니다 CRR(capture/replay/restore) 모드。
사용 방법은 세 가지다:
1 ### 수식 Runnable 및 Callable

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent"); 
Runnable task = new RunnableTask(); 
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task); 
executorService.submit(ttlRunnable);  
// =====================================================  
// Task中可以读取,值是"value-set-in-parent" 
String value = context.get();

2 스레드 풀 꾸미기

ExecutorService executorService = ... 
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);  
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();  
// =====================================================  
// 在父线程中设置 
context.set("value-set-in-parent");  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// =====================================================  
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

3 ### 사용 Java Agent 로 수식하기 JDK 스레드 풀 구현 클래스

// ## 1. 框架上层逻辑,后续流程框架调用业务 ## 
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); 
context.set("value-set-in-parent");  
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ## 
ExecutorService executorService = Executors.newFixedThreadPool(3);  
Runnable task = new RunnableTask(); 
Callable call = new CallableTask(); 
executorService.submit(task); 
executorService.submit(call);  
// ## 3. 框架下层逻辑 ## 
// Task或是Call中可以读取,值是"value-set-in-parent" 
String value = context.get();

java agent 방식을 사용하는 장점은 애플리케이션 코드를 수정할 필요가 없다는 것입니다. 단점으로는 JVM을 시작할 때 매개변수 추가를 잊으면 장애가 발생할 수도 있다는 것이 가장 큰 문제라고 생각합니다.

소스 코드의 로직을 간단히 살펴보자
3.png

4.png 

문제 B는 사실 애플리케이션 개발에서는 대개 고려하지 않는다. 시간 효율에 그만큼 민감하지 않기 때문이다. 하지만 일부 하위 계층의 미들웨어 개발에서는 성능 최적화를 비교적 중요하게 여길 수 있다. 예를 들어 netty는 직접 구현하는 방식을 선택했다FastThreadLocaljdk의 ThreadLocal을 대체하면서, 함께 사용할 수 있는 것도 제공했다 FastThreadLocalThread,에서FastThreadLocalThread및FastThreadLocal함께 사용하면 항상 O(1)의 조회 시간 복잡도를 보장할 수 있습니다. 이 해결책은 공간을 사용해 시간을 절약하는 원리로, 해시 테이블에서 해시 충돌이 발생해 조회 효율이 떨어지는 것을 방지하기 위해 충분히 긴 배열을 생성하며, 각FastThreadLocal생성할 때마다 고유한 배열 인덱스가 할당된다. 한 사람에게 자리 하나씩을 배정하는 셈으로,조회할 때는 자신의 index로 배열에서 바로 가져오면 된다.
소스 코드를 간단히 첨부하겠습니다
5.png


댓글

보내는 중

원하는 테마를 선택하세요

선택한 테마가 사이트 전체에 적용됩니다.