자바 의 ThreadLocal 이해

10289 단어 JavaThreadLocal
머리말
면접 을 볼 때 ThreadLocal 에 관 한 지식 을 물 었 는데 대답 을 잘 못 했 어 요.
ThreadLocal 소개
ThreadLocal 은 JDK 1.2 부터 스 레 드 로 컬 변 수 를 저장 하 는 클래스 입 니 다.ThreadLocal 의 변 수 는 모든 스 레 드 에 독립 적 으로 존재 합 니 다.여러 스 레 드 가 ThreadLocal 의 변 수 를 방문 할 때 사실은 자신의 현재 스 레 드 메모리 에 있 는 변 수 를 방문 하여 변수의 스 레 드 안전 을 보장 합 니 다.
우 리 는 일반적으로 ThreadLocal 을 사용 할 때 스 레 드 에 존재 하 는 변수 경쟁 문 제 를 해결 하기 위해 서 입 니 다.사실 이런 문 제 를 해결 하려 면 보통 synchronized 를 사용 하여 자 물 쇠 를 넣 어 해결 할 생각 을 하 게 됩 니 다.
예 를 들 어 Simple DateFormat 의 스 레 드 가 안전 할 때Simple DateFormat 은 비 스 레 드 가 안전 합 니 다.format()방법 이 든 parse()방법 이 든 모두 자신의 내부 에 있 는 Calendar 류 를 사용 하 는 대상 이 있 습 니 다.format 방법 은 시간 을 설정 하 는 것 입 니 다.parse()방법 은 Calendar 의 clear()방법 을 먼저 호출 한 다음 에 Calendar 의 set()방법(할당)을 호출 했 습 니 다.만약 에 한 스 레 드 가 set()를 호출 하여 값 을 부여 하면이때 또 하나의 스 레 드 가 와 서 clear()방법 을 직접 호출 하면 이 parse()방법 이 실 행 된 결과 에 문제 가 생 길 것 이다.
해결 방법
Simple Dateformat 를 사용 하 는 방법 에 synchronized 를 추가 하면 스 레 드 의 안전 을 보장 하지만 효율 을 떨 어 뜨 립 니 다.같은 시간 에 하나의 스 레 드 만 포맷 시간 을 사용 할 수 있 습 니 다.

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static synchronized String formatDate(Date date){
  return simpleDateFormat.format(date);
}
해결 방법
Simple DateFormat 의 대상 을 ThreadLocal 에 넣 으 면 모든 스 레 드 에 자신의 형식 대상 의 사본 이 있 습 니 다.서로 간섭 하지 않 아 스 레 드 안전 을 보장 했다.

private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static String formatDate(Date date){
  return simpleDateFormatThreadLocal.get().format(date);
}
ThreadLocal 의 원리
우선 ThreadLocal 을 어떻게 사용 하 는 지 살 펴 보 겠 습 니 다.

ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
threadLocal99.set(3);
int num = threadLocal99.get();
System.out.println("  :"+num);
threadLocal99.remove();
System.out.println("  Empty:"+threadLocal99.get());
실행 결과:
숫자:3
숫자 Empty:null
사용 하기 가 간단 합 니 다.주로 ThreadLocal 에 변 수 를 넣 습 니 다.스 레 드 실행 과정 에서 찾 을 수 있 습 니 다.실행 이 끝 난 후에 remove 에서 떨 어 뜨리 면 됩 니 다.reove()를 호출 하지 않 으 면 현재 스 레 드 는 실행 과정 에서 변수 데 이 터 를 얻 을 수 있 습 니 다.
현재 실행 중인 스 레 드 에 넣 었 기 때문에 ThreadLocal 의 변수 값 은 현재 스 레 드 에서 만 사용 할 수 있 고 스 레 드 안전 을 보장 합 니 다(현재 스 레 드 의 하위 스 레 드 도 사실 얻 을 수 있 습 니 다).
ThreadLocal 의 set()방법 원본 을 살 펴 보 겠 습 니 다.

public void set(T value) {
  //       
  Thread t = Thread.currentThread();
  //   ThreadLocalMap
  ThreadLocal.ThreadLocalMap map = getMap(t);
  // ThreadLocalMap       ,            ThreadLocalMap 
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value); // ThreadLocalMap    ,      ,   。
}
변 수 는 모두 ThreadLocalMap 이라는 변수 에 저 장 된 것 을 보 았 습 니 다.그럼 스 레 드 로 컬 맵 은 어떻게 왔 나 요?

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

public class Thread implements Runnable {
	... ...
	/* ThreadLocal values pertaining to this thread. This map is maintained
   * by the ThreadLocal class. */
  ThreadLocal.ThreadLocalMap threadLocals = null;
  ... ...
}
위의 소스 코드 를 통 해 우 리 는 ThreadLocalMap 변 수 는 현재 실행 스 레 드 의 변수 라 는 것 을 알 게 되 었 습 니 다.그래서 ThreadLocal 에 저 장 된 데 이 터 는 모두 현재 실행 스 레 드 의 한 변수 에 넣 었 습 니 다.즉,현재 스 레 드 대상 에 저 장 된 것 입 니 다.다른 스 레 드 안 은 다른 스 레 드 대상 입 니 다.다른 스 레 드 대상 의 데 이 터 를 가 져 올 수 없 기 때문에 데 이 터 는 자 연 스 럽 게 분리 되 었 습 니 다.
그럼 ThreadLocalMap 은 데 이 터 를 어떻게 저장 합 니까?
ThreadLocalMap 은 ThreadLocal 클래스 의 내부 클래스 로 클래스 의 이름 에 Map 이 있 지만 Map 인 터 페 이 스 를 실현 하지 못 하고 구조 와 Map 이 유사 할 뿐이다.

ThreadLocalMap 내 부 는 사실 Entry 배열 입 니 다.Entry 는 ThreadLocalMap 의 내부 클래스 로 Weak Reference 를 계승 하고 ThreadLocal 형식의 대상 을 Entry 의 Key 로 설정 하 며 Key 설정 을 약 한 참조 로 설정 합 니 다.
ThreadLocalMap 의 내부 데이터 구 조 는 대략 이러한 key,value 로 구 성 된 Entry 의 배열 집합 입 니 다.

진정한 맵 과 는 차이 가 있 습 니 다.링크 가 없습니다.그러면 key 의 hash 충돌 을 해결 할 때 조 치 는 HashMap 과 다 를 것 입 니 다.
하나의 스 레 드 에 여러 개의 ThreadLocal 대상 을 만 들 수 있 습 니 다.여러 개의 ThreadLocal 대상 은 여러 개의 데 이 터 를 저장 합 니 다.그러면 ThreadLocalMap 에 이 데 이 터 를 배열 로 저장 합 니 다.
구체 적 인 ThreadLocalMap 의 set()방법의 소스 코드 를 살 펴 보 겠 습 니 다.

/**
 * Set the value associated with key.
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

  // We don't use a fast path as with get() because it is at
  // least as common to use set() to create new entries as
  // it is to replace existing ones, in which case, a fast
  // path would fail more often than not.

  Entry[] tab = table;
  int len = tab.length;
  //          
  int i = key.threadLocalHashCode & (len-1);

  for (Entry e = tab[i];
     e != null;
     e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
    //          ,       key     key  ,             
    if (k == key) {
      e.value = value;
      return;
    }
    //         ,      Entry  ,      。
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  //          ,       key        key ,           ,              。
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}
우 리 는 set()방법 에서 볼 수 있 듯 이 처리 논 리 는 네 단계 가 있다.
  • 첫 번 째 단 계 는 Threadlocal 대상 의 hashcode 와 배열 의 길이 에 따라 연산 을 하여 데 이 터 를 현재 배열 의 위치 에 두 어야 합 니 다
  • 두 번 째 단 계 는 현재 위치 가 비어 있 는 지 판단 하 는 것 입 니 다.비어 있 으 면 Entry 대상 을 직접 초기 화하 고 현재 위치 에 두 는 것 입 니 다
  • 세 번 째 단 계 는 현재 위치 가 비어 있 지 않 고 현재 위치 에 있 는 Entry 의 key 가 전 달 된 key 와 같다 면 현재 위치 에 있 는 데 이 터 를 직접 덮어 씁 니 다
  • 네 번 째 단 계 는 현재 위치 가 비어 있 지 않 고 현재 위치 에 있 는 Entry 의 key 와 전 달 된 key
  • 4.567917 도 다 르 기 때문에 다음 빈 위 치 를 찾 아 빈 위치 에 데 이 터 를 저장 합 니 다(배열 이 길 이 를 초과 하면 확장 을 실행 합 니 다)get 을 할 때 도 비슷 한 논리 입 니 다.먼저 들 어 오 는 ThreadLocal 의 hashcode 를 통 해 Entry 배열 에 있 는 위 치 를 가 져 온 다음 현재 위치 에 있 는 Entry 의 Key 와 들 어 오 는 ThreadLocal 을 비교 하면 데 이 터 를 되 돌려 줍 니 다.같 지 않 으 면 배열 의 다음 값 의 key 와 같 는 지 판단 합 니 다.
    
    private Entry getEntry(ThreadLocal<?> key) {
      int i = key.threadLocalHashCode & (table.length - 1);
      Entry e = table[i];
      if (e != null && e.get() == key)
        return e;
      else
        return getEntryAfterMiss(key, i, e);
    }
    
    /**
     * Version of getEntry method for use when key is not found in
     * its direct hash slot.
     *
     * @param key the thread local object
     * @param i the table index for key's hash code
     * @param e the entry at table[i]
     * @return the entry associated with key, or null if no such
     */
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
      Entry[] tab = table;
      int len = tab.length;
    
      while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
          return e;
        if (k == null)
          expungeStaleEntry(i);
        else
          i = nextIndex(i, len);
        e = tab[i];
      }
      return null;
    }
    우 리 는 앞에서 말 했 듯 이 ThreadLocal 은 하나의 스 레 드 에 저 장 된 데이터 로 모든 스 레 드 는 자신의 데 이 터 를 가지 고 있 지만 실제 ThreadLocal 안의 진정한 대상 데 이 터 는 사실은 더미 안에 저 장 된 것 이 고 스 레 드 에는 대상 의 인용 만 저장 되 어 있 을 뿐이다.
    그리고 우리 가 사용 할 때 보통 이전 스 레 드 에서 실 행 된 방법의 상하 문 에서 ThreadLocal 의 변 수 를 공유 해 야 합 니 다.
    예 를 들 어 제 메 인 스 레 드 는 어떤 방법 에서 코드 를 실행 하 는 것 입 니 다.그러나 이 방법 에는 코드 가 있 을 때 스 레 드 를 새로 만 들 었 습 니 다.이 스 레 드 에 서 는 실행 중인 방법 에서 정 의 된 ThreadLocal 의 변 수 를 사 용 했 습 니 다.이 럴 때 는 새 스 레 드 에서 외부 스 레 드 의 데 이 터 를 호출 해 야 합 니 다.이 는 스 레 드 간 공유 가 필요 합 니 다.이 피 드 부모 스 레 드 가 데 이 터 를 공유 하 는 경우 ThreadLocal 도 지원 합 니 다.
    예 를 들 면:
    
     ThreadLocal threadLocalMain = new InheritableThreadLocal();
     threadLocalMain.set("     ");
     Thread t = new Thread() {
       @Override
       public void run() {
         super.run();
         System.out.println( "         =" + threadLocalMain.get());
       }
     };
     t.start();
    실행 결과:
    현재 가 져 온 변 수 는=주 스 레 드 변수 입 니 다.
    위의 이러한 코드 는 하위 부모 스 레 드 가 데 이 터 를 공유 하 는 상황 을 실현 할 수 있 으 며,중요 한 것 은 Inheritable Thread Local 을 사용 하여 공유 하 는 것 입 니 다.
    그렇다면 이 는 어떻게 데이터 공 유 를 실현 합 니까?
    Thread 클래스 의 init()방법 에는 다음 과 같은 코드 가 있 습 니 다.
    
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
          this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    이 코드 는 스 레 드 를 만 들 때 현재 스 레 드 의 inheritThreadLocals 변수 와 부모 스 레 드 의 inheritThreadLocals 변수 가 비어 있 지 않 을 때 부모 스 레 드 의 inheritThreadLocals 변수 에 있 는 데 이 터 를 현재 스 레 드 의 inheritThreadLocals 변수 에 부여 한 다 는 뜻 입 니 다.
    ThreadLocal 메모리 누 출 문제
    앞에서 도 언급 했 듯 이 ThreadLocal 의 ThreadLocalMap 에 있 는 Entry 대상 은 Weak Reference 류 를 계승 하 는 것 으로 Entry 의 key 가 약 한 인용 임 을 나타 낸다.

    약 인용 은 필요 하지 않 은 대상,약 인용 대상 을 묘사 하 는 것 으로 다음 쓰레기 수집 이 발생 할 때 까지 만 생존 할 수 있다.쓰레기 수집 기 가 작업 을 시작 하면 현재 메모리 가 충분 하 든 상 관 없 이 약 한 인용 대상 만 회수 합 니 다.
    이 약 한 인용 은 ThreadLocal 대상 자체 이기 때문에 스 레 드 실행 이 완료 되면 ThreadLocal 대상 은 null 이 되 고 null 의 약 한 인용 대상 은 다음 GC 에서 삭 제 됩 니 다.그러면 Entry 의 Key 메모리 공간 이 방출 되 지만 Entry 의 value 는 사용 하 는 메모리 입 니 다.만약 에 스 레 드 가 재 활용 된다 면(예 를 들 어 스 레 드 탱크 의 스 레 드)이 안의 value 값 이 점점 많아 지고 결국은 메모리 가 새 게 될 것 이다.
    메모리 누 출 을 방지 하 는 방법 은 ThreadLocal 을 사용 할 때마다 다음 reove()방법 을 실행 하면 key 와 value 의 공간 을 모두 방출 할 수 있 습 니 다.
    그렇다면 메모리 누 출 이 발생 하기 쉬 운 데 왜 약 한 인용 으로 설정 해 야 합 니까?
    정상 적 인 상황 에 서 는 강 한 인용 이 어야 하지만 강 한 인용 관 계 를 가지 고 있 으 면 회수 되 지 않 기 때문에 스 레 드 가 재 활용 되면 Entry 의 Key 와 Value 는 회수 되 지 않 아 Key 와 Value 모두 메모리 누 출 이 발생 할 수 있 습 니 다.
    이상 은 자바 에서 ThreadLocal 에 대한 이해 의 상세 한 내용 입 니 다.자바 ThreadLocal 에 관 한 자 료 는 저희 의 다른 관련 글 을 주목 하 세 요!

    좋은 웹페이지 즐겨찾기