>Java >java지도 시간 >Java 프로그래밍에서 ThreadLocal에 대한 자세한 설명 및 소스 코드 분석

Java 프로그래밍에서 ThreadLocal에 대한 자세한 설명 및 소스 코드 분석

WBOY
WBOY앞으로
2023-04-21 15:19:081603검색

    Introduction

    ThreadLocal은 멀티 스레드 환경에서 각 스레드가 고유한 데이터를 가질 수 있고 전체 스레드 실행 프로세스 동안 위에서 아래로 전달될 수 있는 방법을 제공합니다.

    1. 사용법 데모

    ThreadLocal을 사용해 본 적이 없는 학생들이 많을 것입니다. 먼저 ThreadLocal의 사용법을 보여드리겠습니다.

    /**
     * ThreadLocal 中保存的数据是 Map
     */
    static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
    @Test
    public void testThread() {
      // 从上下文中拿出 Map
      Map<String, String> contextMap = context.get();
      if (CollectionUtils.isEmpty(contextMap)) {
        contextMap = Maps.newHashMap();
      }
      contextMap.put("key1", "value1");
      context.set(contextMap);
      log.info("key1,value1被放到上下文中");
    	// 从上下文中拿出刚才放进去的数据
      getFromComtext();
    }
    private String getFromComtext() {
      String value1 = context.get().get("key1");
      log.info("从 ThreadLocal 中取出上下文,key1 对应的值为:{}", value1);
      return value1;
    }
    //运行结果:
    demo.ninth.ThreadLocalDemo - key1,value1被放到上下文中
    demo.ninth.ThreadLocalDemo - 从 ThreadLocal 中取出上下文,key1 对应的值为:value1

    실행 결과에서 볼 수 있듯이 key1에 해당하는 값은 다음과 같습니다. 문맥에서 얻었습니다.

    getFromComtext 메소드는 어떤 입력 매개변수도 허용하지 않습니다. context.get().get("key1") 코드 라인을 통해 컨텍스트에서 key1 값을 가져오는 방법을 살펴보겠습니다. 기본 ThreadLocal이 구현됩니다.

    2. 클래스 구조

    2.1. 클래스 generics

    ThreadLocal은 ThreadLocal이 어떤 형식으로든 데이터를 저장할 수 있음을 의미합니다.

    public class ThreadLocal<T> {}

    2.2. 여러 키 속성을 하나씩 살펴보겠습니다.

    // threadLocalHashCode 表示当前 ThreadLocal 的 hashCode,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
    private final int threadLocalHashCode = nextHashCode();
    // 计算 ThreadLocal 的 hashCode 值(就是递增)
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    // static + AtomicInteger 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
    // 被 static 修饰非常关键,因为一个线程在处理业务的过程中,ThreadLocalMap 是会被 set 多个 ThreadLocal 的,多个 ThreadLocal 就依靠 threadLocalHashCode 进行区分
    private static AtomicInteger nextHashCode = new AtomicInteger();

    또 다른 중요한 속성인 ThreadLocalMap이 있습니다. 스레드에 여러 ThreadLocal이 있는 경우 여러 ThreadLocal을 관리하는 컨테이너가 필요합니다. ThreadLocalMap의 역할은 여러 ThreadLocal을 관리하는 것입니다. 스레드에서.

    2.2.1, ThreadLocalMap

    ThreadLocalMap 자체는 간단한 Map 구조, 키는 ThreadLocal, 값은 ThreadLocal이 저장한 값, 맨 아래 레이어는 배열의 데이터 구조, 소스 코드는 다음과 같습니다.
    // threadLocalHashCode 表示当前 ThreadLocal 的 hashCode,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置
    private final int threadLocalHashCode = nextHashCode();
    // 计算 ThreadLocal 的 hashCode 值(就是递增)
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    // static + AtomicInteger 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的
    // 被 static 修饰非常关键,因为一个线程在处理业务的过程中,ThreadLocalMap 是会被 set 多个 ThreadLocal 的,多个 ThreadLocal 就依靠 threadLocalHashCode 进行区分
    private static AtomicInteger nextHashCode = new AtomicInteger();

    ThreadLocalMap은 실제로 간단한 Map 구조이며, 맨 아래 레이어는 초기화 크기와 확장 임계값 크기를 갖는 배열입니다. 배열의 요소는 Entry이고, Entry의 키는 ThreadLocal에 대한 참조입니다. 이고 값은 ThreadLocal의 값입니다.

    3. ThreadLocal은 스레드 간 데이터 격리를 어떻게 달성합니까?

    ThreadLocal은 스레드로부터 안전하며 주로 ThreadLocalMap이 스레드의 속성이기 때문에 안심하고 사용할 수 있습니다.

    Java 프로그래밍에서 ThreadLocal에 대한 자세한 설명 및 소스 코드 분석위 그림에서 ThreadLocals.ThreadLocalMap과 InheritableThreadLocals.ThreadLocalMap이 스레드의 속성이므로 각 스레드의 ThreadLocal이 격리되고 배타적이라는 것을 알 수 있습니다.

    상위 스레드가 하위 스레드를 생성하면 상속 가능한ThreadLocals 값은 복사되지만 threadLocals 값은 복사되지 않습니다. 소스 코드는 다음과 같습니다.

    Java 프로그래밍에서 ThreadLocal에 대한 자세한 설명 및 소스 코드 분석위 그림에서 우리는 스레드가 생성되면 상위 스레드의 InheritableThreadLocals 속성 값이 복사됩니다.

    4. Set 메소드

    set 메소드의 주요 기능은 현재 ThreadLocal의 일반 유형이 Map인 경우 소스에 맵을 설정하는 것입니다.

    // set 操作每个线程都是串行的,不会有线程安全的问题
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 当前 thradLocal 之前有设置值,直接设置,否则初始化
        if (map != null)
            map.set(this, value);
        // 初始化ThreadLocalMap
        else
            createMap(t, value);
    }

    코드 논리는 비교적 명확합니다. ThreadLocalMap.set의 소스 코드를 살펴보면 다음과 같습니다.

    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        // 计算 key 在数组中的下标,其实就是 ThreadLocal 的 hashCode 和数组大小-1取余
        int i = key.threadLocalHashCode & (len-1);
     
        // 整体策略:查看 i 索引位置有没有值,有值的话,索引位置 + 1,直到找到没有值的位置
        // 这种解决 hash 冲突的策略,也导致了其在 get 时查找策略有所不同,体现在 getEntryAfterMiss 中
        for (Entry e = tab[i];
             e != null;
             // nextIndex 就是让在不超过数组长度的基础上,把数组的索引位置 + 1
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            // 找到内存地址一样的 ThreadLocal,直接替换
            if (k == key) {
                e.value = value;
                return;
            }
            // 当前 key 是 null,说明 ThreadLocal 被清理了,直接替换掉
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 当前 i 位置是无值的,可以被当前 thradLocal 使用
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 当数组大小大于等于扩容阈值(数组大小的三分之二)时,进行扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

    위 소스 코드에서 몇 가지 사항에 주목합니다.

      증가하는 AtomicInteger를 ThreadLocal의 hashCode로 사용합니다.
    • 배열 인덱스 위치를 계산하는 공식은 다음과 같습니다. hashCode 모듈로 hashCode가 계속 증가하므로 다른 hashCode가 인덱스 위치를 계산할 가능성이 높습니다. 동일한 배열(그러나 이에 대해 걱정하지 마십시오. 실제 프로젝트에서 ThreadLocal은 매우 드물며 기본적으로 충돌이 아닙니다.)
    • hashCode로 계산한 인덱스 위치에 이미 값이 있으면 i에서 시작하고 빈 인덱스 위치를 찾을 때까지 +1을 통해 계속해서 뒤로 검색하고 현재 ThreadLocal을 키로 넣습니다.
    • 다행히 일상 업무에서 ThreadLocal을 사용할 때 1~2개의 ThreadLocal만 사용하는 경우가 많고, 해싱을 통해 계산된 중복 배열의 확률도 그리 높지 않습니다.

    set 중 배열 요소 위치 충돌을 해결하기 위한 전략은 get 메서드에도 영향을 미칩니다. get 메서드를 함께 살펴보겠습니다.

    5. get 메소드

    get 메소드는 주로 ThreadLocalMap에서 현재 ThreadLocal에 저장된 값을 가져옵니다.

    public T get() {
        // 因为 threadLocal 属于线程的属性,所以需要先把当前线程拿出来
        Thread t = Thread.currentThread();
        // 从线程中拿到 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 从 map 中拿到 entry,由于 ThreadLocalMap 在 set 时的 hash 冲突的策略不同,导致拿的时候逻辑也不太一样
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果不为空,读取当前 ThreadLocal 中保存的值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 否则给当前线程的 ThreadLocal 初始化,并返回初始值 null
        return setInitialValue();
    }

    그럼 ThreadLocalMap의 getEntry 메소드를 살펴보겠습니다.

    // 得到当前 thradLocal 对应的值,值的类型是由 thradLocal 的泛型决定的
    // 由于 thradLocalMap set 时解决数组索引位置冲突的逻辑,导致 thradLocalMap get 时的逻辑也是对应的
    // 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止
    private Entry getEntry(ThreadLocal<?> key) {
        // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getEntryAfterMiss 方法找
        if (e != null && e.get() == key)
            return e;
        else
        // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的  
            return getEntryAfterMiss(key, i, e);
    }
    // 自旋 i+1,直到找到为止
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的
        while (e != null) {
            ThreadLocal<?> k = e.get();
            // 内存地址一样,表示找到了
            if (k == key)
                return e;
            // 删除没用的 key
            if (k == null)
                expungeStaleEntry(i);
            // 继续使索引位置 + 1
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

    get logic 소스 코드의 설명 매우 명확하게 작성되었으므로 다시 반복하지 않겠습니다.

    6. 확장

    ThreadLocalMap의 ThreadLocal 수가 임계값을 초과하면 ThreadLocalMap이 확장되기 시작합니다. 확장 논리를 살펴보겠습니다.

    //扩容
    private void resize() {
        // 拿出旧的数组
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        // 新数组的大小为老数组的两倍
        int newLen = oldLen * 2;
        // 初始化新数组
        Entry[] newTab = new Entry[newLen];
        int count = 0;
        // 老数组的值拷贝到新数组上
        for (int j = 0; j < oldLen; ++j) {
            Entry e = oldTab[j];
            if (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null; // Help the GC
                } else {
                    // 计算 ThreadLocal 在新数组中的位置
                    int h = k.threadLocalHashCode & (newLen - 1);
                    // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    // 给新数组赋值
                    newTab[h] = e;
                    count++;
                }
            }
        }
        // 给新数组初始化下次扩容阈值,为数组长度的三分之二
        setThreshold(newLen);
        size = count;
        table = newTab;
    }

    소스 코드 주석도 비교적 명확합니다. 두 가지 점:

      확장 후 배열 크기는 원래 배열의 두 배입니다.
    • ThreadLocalMap은 스레드의 속성이고 스레드는 확장 중에 스레드 안전 문제가 전혀 없습니다. 동일한 스레드에서는 비즈니스 로직의 실행이 직렬이어야 하므로 ThreadLocalMap의 작업도 직렬이어야 하기 때문에 동시에 ThreadLocalMap에서만 작동합니다.

    위 내용은 Java 프로그래밍에서 ThreadLocal에 대한 자세한 설명 및 소스 코드 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

    성명:
    이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제