当前位置: 首页 > news >正文

Java ThreadLocal

“三年前我也曾被这个技术点卡住… 你好,我是曾续缘。今天把踩坑经验转化成这份避坑指南,关注我,让我的弯路变成你的捷径🛣️”

ThreadLocal是Java提供的一个线程局部变量工具类,在java.lang包中,它允许每个线程拥有自己的变量副本,从而实现线程间的数据隔离。

内部类

在ThreadLocal类中,有2个关键的内部类,它们共同构成了ThreadLocal的核心功能。

SuppliedThreadLocal

这是一个继承自ThreadLocal的静态内部类,通过构造函数接收一个Supplier<? extends T>类型的参数,这个Supplier用于在ThreadLocal变量第一次被访问时提供一个初始值。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}
  • 属性
    • supplier: 类型为Supplier<? extends T>,用于提供初始值。
  • 方法
    • initialValue(): 重写了ThreadLocal的initialValue方法,该方法会在ThreadLocal变量第一次使用时调用,并返回由supplier提供的值。

ThreadLocalMap

这是一个静态内部类,用于存储与线程相关的ThreadLocal变量值。它是一个定制化的哈希表,专门用于维护线程局部变量值,作为线程对象Thread的一个属性,因此每个线程对象都有属于自己的ThreadLocalMap,做到了天然的数据隔离。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    private int threshold; // Default to 0
    // ...
}

属性

  • INITIAL_CAPACITY: 表的初始容量,必须为2的幂。
  • table: 类型为Entry[],是实际的哈希表,用于存储键值对。
  • size: 表中条目的数量。
  • threshold: 下一个调整大小的阈值。

内部类

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Entry: 这是一个继承自WeakReference<ThreadLocal<?>>的静态内部类,用于表示ThreadLocalMap中的一个条目。它包含一个ThreadLocal对象的弱引用作为,和一个与该ThreadLocal关联的

构造函数:接收一个ThreadLocal对象和一个值作为参数,创建一个弱引用指向ThreadLocal对象,并将值保存在value属性中。

属性

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在Java的ThreadLocal类中有3个关键属性,它们帮助实现线程局部变量的功能:

threadLocalHashCode

  • 类型int
  • 作用:这是一个final属性,代表当前ThreadLocal实例的唯一哈希码。这个哈希码用于在ThreadLocalMap中作为键,以便为每个线程存储和检索其对应的值。该值在对象创建时通过调用nextHashCode()方法进行初始化,并且之后不会再改变。

nextHashCode

  • 类型AtomicInteger
  • 作用:这是一个静态属性,用于生成下一个ThreadLocal实例的哈希码。

AtomicInteger确保了在多线程环境中对nextHashCode的修改是原子的,从而保证了每个ThreadLocal实例都有唯一的哈希码。初始值为0,每次创建新的ThreadLocal实例时,都会通过nextHashCode()方法增加HASH_INCREMENT

HASH_INCREMENT

  • 类型int
  • 作用:这是一个静态常量,用于确定连续生成的哈希码之间的增量。这个特殊的值0x61c88647被选择是因为它能够提供良好的哈希分布,减少哈希冲突。

nextHashCode()

  • 类型static int
  • 作用:这是一个静态方法,用于获取并递增nextHashCode的值。每次调用此方法时,都会返回当前的nextHashCode值,并将其增加HASH_INCREMENT
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里的getAndAdd(int delta)方法实际上是Unsafe类的getAndAddInt方法,用于执行原子的整数加法操作。

哈希码的生成和管理确保了线程局部变量在ThreadLocalMap中的唯一性和可访问性。

主要方法

get方法

get方法用于检索当前线程的线程局部变量的值。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

以下是ThreadLocalget方法的执行流程。

  1. 获取当前线程get方法首先调用Thread.currentThread()来获取当前正在执行的线程。

  2. 获取线程的ThreadLocalMap:通过调用getMap(t)方法,获取当前线程的threadLocals变量,这是一个ThreadLocalMap类型的变量。每个线程存储着一个ThreadLocalMap引用。

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 从ThreadLocalMap中获取条目:如果ThreadLocalMap不为null,则调用map.getEntry(this),其中this是当前的ThreadLocal实例。这个方法尝试获取与当前ThreadLocal实例关联的条目(Entry)。

    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.refersTo(key))
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
  4. 检查条目:如果找到了条目(Entry),则检查条目的键是否确实是指向当前的ThreadLocal实例。如果是,将条目的值(e.value)转型为泛型类型T并返回。

  5. 处理未命中情况:如果在ThreadLocalMap中没有找到对应的条目,或者条目已经无效(即键为null),则会调用getEntryAfterMiss方法。这个方法会遍历哈希表,以处理哈希冲突的情况,并且在这个过程中清理掉无效的条目。

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
        while (e != null) {
            if (e.refersTo(key))
                return e;
            if (e.refersTo(null))
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
    
  6. 清理无效条目:如果getEntryAfterMiss方法在遍历过程中遇到了无效的条目(键为null),则会调用expungeStaleEntry方法来清理这个无效的条目,并且可能重新哈希其他条目。

    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
        // expunge entry at staleSlot
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;
    
        // Rehash until we encounter null
        Entry e;
        int i;
        for (i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null;
                tab[i] = null;
                size--;
            } else {
                int h = k.threadLocalHashCode & (len - 1);
                if (h != i) {
                    tab[i] = null;
    
                    // Unlike Knuth 6.4 Algorithm R, we must scan until
                    // null because multiple entries could have been stale.
                    while (tab[h] != null)
                        h = nextIndex(h, len);
                    tab[h] = e;
                }
            }
        }
        return i;
    }
    
  7. 设置初始值:如果ThreadLocalMapnull或者没有找到对应的条目,get方法会调用setInitialValue()。这个方法首先调用initialValue()来获取初始值,这个方法默认返回null,但可以在子类中被重写以提供非null的初始值。

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }
    protected T initialValue() {
        return null;
    }
    
  8. 创建ThreadLocalMap:如果当前线程的threadLocals变量为null,则调用createMap方法来创建一个新的ThreadLocalMap,并将初始值存入。

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
  9. 返回初始值setInitialValue方法最后返回初始值,这个值也会被get方法返回。

在整个过程中,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);
    }
}

以下是ThreadLocalset方法的执行流程:

  1. 获取当前线程set方法首先调用Thread.currentThread()来获取当前正在执行的线程。

  2. 获取线程的ThreadLocalMap:通过调用getMap(t)方法,获取当前线程的threadLocals变量,这是一个ThreadLocalMap类型的变量。

  3. 检查ThreadLocalMap是否为空

    • 如果ThreadLocalMap不为null,则继续执行。
    • 如果ThreadLocalMapnull,则调用createMap(t, value)创建一个新的ThreadLocalMap,并将当前ThreadLocal实例和要设置的值作为初始键值对存入。
  4. 设置值:调用map.set(this, value)来设置值。

    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)]) {
            if (e.refersTo(key)) {
                e.value = value;
                return;
            }
    
            if (e.refersTo(null)) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    
  5. 遍历哈希表

    从计算出的索引位置开始,如果当前位置的条目(Entry)不为null,则进行以下检查:

    • 检查条目是否匹配:如果当前条目的键(ThreadLocal实例)与this相等,则更新该条目的值为新值,并结束方法。

    • 检查条目是否无效:如果当前条目的键为null(表示条目无效),则调用replaceStaleEntry方法来替换无效的条目,并结束方法。

      private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
          Entry[] tab = table;
          int len = tab.length;
          Entry e;
      
          // Back up to check for prior stale entry in current run.
          // We clean out whole runs at a time to avoid continual
          // incremental rehashing due to garbage collector freeing
          // up refs in bunches (i.e., whenever the collector runs).
          int slotToExpunge = staleSlot;
          for (int i = prevIndex(staleSlot, len);
               (e = tab[i]) != null;
               i = prevIndex(i, len))
              if (e.refersTo(null))
                  slotToExpunge = i;
      
          // Find either the key or trailing null slot of run, whichever
          // occurs first
          for (int i = nextIndex(staleSlot, len);
               (e = tab[i]) != null;
               i = nextIndex(i, len)) {
              // If we find key, then we need to swap it
              // with the stale entry to maintain hash table order.
              // The newly stale slot, or any other stale slot
              // encountered above it, can then be sent to expungeStaleEntry
              // to remove or rehash all of the other entries in run.
              if (e.refersTo(key)) {
                  e.value = value;
      
                  tab[i] = tab[staleSlot];
                  tab[staleSlot] = e;
      
                  // Start expunge at preceding stale entry if it exists
                  if (slotToExpunge == staleSlot)
                      slotToExpunge = i;
                  cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                  return;
              }
      
              // If we didn't find stale entry on backward scan, the
              // first stale entry seen while scanning for key is the
              // first still present in the run.
              if (e.refersTo(null) && slotToExpunge == staleSlot)
                  slotToExpunge = i;
          }
      
          // If key not found, put new entry in stale slot
          tab[staleSlot].value = null;
          tab[staleSlot] = new Entry(key, value);
      
          // If there are any other stale entries in run, expunge them
          if (slotToExpunge != staleSlot)
              cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      }
      
    • 如果以上都不满足,则使用nextIndex方法找到下一个索引位置,继续遍历。

  6. 添加新条目:如果在哈希表中没有找到匹配的条目,则创建一个新的Entry对象,并将其放入计算出的索引位置。

  7. 清理哈希表:在添加新条目后,调用cleanSomeSlots方法来清理哈希表中的无效条目。这个方法会遍历哈希表的一部分,清理遇到的无效条目。

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.refersTo(null)) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }
    
  8. 检查是否需要重新哈希

    • 如果cleanSomeSlots方法没有清理任何条目,并且当前哈希表的大小超过了阈值,则调用rehash方法。

      private void rehash() {
          expungeStaleEntries();
      
          // Use lower threshold for doubling to avoid hysteresis
          if (size >= threshold - threshold / 4)
              resize();
      }
      
    • rehash方法首先调用expungeStaleEntries来清理所有的无效条目。

      private void expungeStaleEntries() {
          Entry[] tab = table;
          int len = tab.length;
          for (int j = 0; j < len; j++) {
              Entry e = tab[j];
              if (e != null && e.refersTo(null))
                  expungeStaleEntry(j);
          }
      }
      
    • 如果清理后的大小仍然大于阈值减去阈值的一部分(通常是阈值减去四分之一),则调用resize方法来扩容哈希表。

        private void resize() {
                  Entry[] oldTab = table;
                  int oldLen = oldTab.length;
                  int newLen = oldLen * 2;
                  Entry[] newTab = new Entry[newLen];
                  int count = 0;
      
                  for (Entry e : oldTab) {
                      if (e != null) {
                          ThreadLocal<?> k = e.get();
                          if (k == null) {
                              e.value = null; // Help the GC
                          } else {
                              int h = k.threadLocalHashCode & (newLen - 1);
                              while (newTab[h] != null)
                                  h = nextIndex(h, newLen);
                              newTab[h] = e;
                              count++;
                          }
                      }
                  }
      
                  setThreshold(newLen);
                  size = count;
                  table = newTab;
              }
      

      resize方法将存储数据的数组扩容为原来长度的两倍,同时重新计算每个元素在新数组中的位置,并更新map的大小和扩容阈值。

  9. 结束set方法执行完毕,新的值已经存储在当前线程的ThreadLocalMap中。

remove方法

以下是remove方法的执行流程:

  1. 获取当前线程的ThreadLocalMapremove方法首先通过调用getMap(Thread.currentThread())来获取当前线程的ThreadLocalMap实例。

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }
    
  2. 检查ThreadLocalMap是否为空:如果ThreadLocalMap不为null,则继续执行删除操作;如果为null,则方法结束,因为没有可删除的条目。

  3. 删除条目:调用m.remove(this),其中this是当前的ThreadLocal实例。以下是remove方法的详细步骤:

    private void remove(ThreadLocal<?> key) {
        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)]) {
            if (e.refersTo(key)) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    
    • 计算哈希索引:通过key.threadLocalHashCode & (len-1)计算当前ThreadLocal实例的哈希索引,其中len是哈希表的长度。
    • 遍历哈希表:从计算出的索引位置开始,如果当前位置的条目(Entry)不为null,则进行以下检查:
      • 检查条目是否匹配:通过调用e.refersTo(key)来检查当前条目的键是否与ThreadLocal实例相等。refersTo方法最终会调用到refersTo0这个本地方法,它是一个@IntrinsicCandidate,表示它可能被JVM内部优化。

        public final boolean refersTo(T obj) {
            return refersToImpl(obj);
        }
        boolean refersToImpl(T obj) {
            return refersTo0(obj);
        }
        @IntrinsicCandidate
        private native boolean refersTo0(Object o);
        
      • 如果条目匹配(即找到了对应的Entry),则调用e.clear()来清除条目的值,然后调用expungeStaleEntry(i)来清理当前索引位置的条目,并结束方法。

      • 如果条目不匹配,则使用nextIndex方法找到下一个索引位置,继续遍历。

  4. 清理无效条目expungeStaleEntry(i)方法会将当前索引位置的条目置为null,并重新哈希后续的条目,同时清理掉所有遇到的无效条目(键为null的条目)。

  5. 结束:如果遍历完整个哈希表都没有找到匹配的条目,则remove方法结束,没有进行任何操作。

哈希冲突解决

在Java的ThreadLocal实现中,哈希冲突的解决主要依赖于哈希表的动态调整和重哈希机制。当两个不同的ThreadLocal实例通过哈希函数映射到同一个哈希桶(bucket)时,就会发生哈希冲突。

ThreadLocal使用了一个开放定址法(Open Addressing)的策略来解决哈希冲突。

  1. 使用哈希码与桶数组大小取模ThreadLocal使用哈希码与桶数组大小取模的方式来确定哈希桶的位置。这种方法可以有效减少哈希冲突,因为不同的键通过取模操作通常会分散到不同的桶中。
  2. 线性探测:当发生哈希冲突时,ThreadLocal使用线性探测(Linear Probing)的方式来解决冲突。这意味着如果在目标桶中发现有其他元素,就会检查下一个桶,如果仍然被占用,就继续检查下一个桶,直到找到一个空桶为止。
  3. 重哈希:当哈希表中的条目数达到阈值时,ThreadLocal会触发重哈希操作。重哈希会创建一个新的更大的哈希表,并将原有的键值对重新插入到新的哈希表中。这个过程有助于进一步减少哈希冲突。
  4. 动态调整桶数组大小ThreadLocal的哈希表是一个动态调整大小的数组。当重哈希操作发生时,数组的大小会翻倍。这种策略有助于在哈希表变得过于拥挤时,通过增加空间来减少冲突。
  5. 阈值调整ThreadLocal的阈值(threshold)是一个控制重哈希操作的关键参数。当哈希表中的条目数超过这个阈值时,就会触发重哈希。这个阈值通常设置为哈希表大小的某个比例,例如2/3外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内存泄漏

ThreadLocalMapThreadLocal 的静态内部类,每个线程对象都持有一个 ThreadLocalMap 实例。

当线程访问 ThreadLocal 变量时,实际上是访问线程自己的 ThreadLocalMap,这个映射表存储了 ThreadLocal 与其对应的数据。

ThreadLocalMap 中的数据是以 Entry 对象的形式存储的,每个 Entry 对象包含一个 ThreadLocal 对象的弱引用作为键(Key)和一个具体值(Value)。

Entry 对象中的键是 ThreadLocal 对象的弱引用。弱引用的特点是,它不会阻止垃圾回收器回收其引用的对象。

ThreadLocal 对象不再有强引用指向它时,垃圾回收器可能会在任何时候回收这个 ThreadLocal 对象。

由于 ThreadLocalMap 中的键是弱引用,一旦 ThreadLocal 对象被回收,Entry 中的键将变为 null,但值仍然存在。

即使 ThreadLocal 对象被回收,ThreadLocalMap 中的 Entry 对象仍然被线程对象强引用,因此 Entry 对象及其值不会立即被回收。

如果线程对象长时间存活(例如,线程池中的线程),那么这些无用的 Entry 对象及其值将一直占用内存,导致内存泄漏。

如果线程是短暂存在的,那么即使发生内存泄漏,线程结束后,其占用的内存也会被回收。

如果线程长时间存在(如线程池中的线程),且没有正确清理 ThreadLocal,则可能导致内存泄漏。

在线程池中,线程可能会被重复使用,这意味着线程的生命周期可能会比单个任务的生命周期长。

ThreadLocalgetsetremove 方法中,ThreadLocalMap 会尝试清理那些键为 nullEntry 对象。

这种清理是有限的,因为它依赖于对这些方法的调用频率,并不能保证立即清理所有的无效 Entry

总结来说,内存泄漏的过程如下:

  1. 线程创建: 当线程创建时,每个线程都会有一个ThreadLocalMap
  2. 访问ThreadLocal: 当线程访问一个ThreadLocal变量时,会在ThreadLocalMap中创建一个Entry对象,其中keyThreadLocal实例的弱引用,valueThreadLocal变量的值。
  3. GC触发: 如果没有任何强引用指向ThreadLocal实例,那么ThreadLocal实例会被垃圾回收,Entry对象的key将变为null
  4. 内存泄漏: 此时,虽然Entrykey已经为null,但由于ThreadLocalMap的存在,Entryvalue仍然保持不变。如果线程继续存活,那么这个Entry将不会被垃圾回收,导致内存泄漏。
    tsetremove 方法中,ThreadLocalMap会尝试清理那些键为nullEntry` 对象。

这种清理是有限的,因为它依赖于对这些方法的调用频率,并不能保证立即清理所有的无效 Entry

总结来说,内存泄漏的过程如下:

  1. 线程创建: 当线程创建时,每个线程都会有一个ThreadLocalMap
  2. 访问ThreadLocal: 当线程访问一个ThreadLocal变量时,会在ThreadLocalMap中创建一个Entry对象,其中keyThreadLocal实例的弱引用,valueThreadLocal变量的值。
  3. GC触发: 如果没有任何强引用指向ThreadLocal实例,那么ThreadLocal实例会被垃圾回收,Entry对象的key将变为null
  4. 内存泄漏: 此时,虽然Entrykey已经为null,但由于ThreadLocalMap的存在,Entryvalue仍然保持不变。如果线程继续存活,那么这个Entry将不会被垃圾回收,导致内存泄漏。

参考文章:https://cengxuyuan.cn

相关文章:

  • LINUX本地磁盘DISK空间扩容
  • SpringBoot 集成nacos,实现动态配置更新、docker安装nacos
  • 排序算法-冒泡排序
  • 计算机毕设-基于springboot的物业管理系统的设计与实现(附源码+lw+ppt+开题报告)
  • GPT-4 Turbo的重大升级与深远影响
  • Zabbix 安装部署
  • Ruby 安装 - Linux
  • 在 Ubuntu 上安装和切换多个 GCC 版本
  • 在Linux系统上集成OpenSlide与SpringBoot
  • HTTPS安全通信协议原理
  • Pytest安装和介绍
  • 【Go】Go zap 日志模块
  • STM32项目分享:STM32智能窗户
  • AI 实战 - pytorch框架基于retinaface实现face检测
  • Spring Boot面试问答
  • Docker 部署 vaultwarden
  • SyntaxError: Unexpected token ‘xxx‘
  • MySQL SELECT 查询性能优化指南
  • 批量将 Word 拆分成多个文件
  • [Vue warn]: Duplicate keys detected: ‘xxx‘. This may cause an update error.
  • 德惠网站建设/个人网站制作软件
  • 网站建设方案设计心得/软件外包公司
  • 网站建设工作流程/公司网络优化方案
  • 如何做免费网站推广/小红书推广怎么收费
  • 西安网站推广都是怎么做的/推广网站
  • 吉安购物网站制作/如何注册域名及网站