掌握Java中的ThreadLocal

ThreadLocal

ThreadLocal是什么?

ThreadLocal是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。

线程安全问题的核心在于多个线程会对同一个临界区的共享资源进行访问,那如果每个线程都拥有自己的“共享资源”,各用各的,互不影响,这样就不会出现线程安全的问题了。事实上,这就是一种“空间换时间”的思想,每个线程拥有自己的“共享资源”,虽然内存占用变大了,但由于不需要同步,也就减少了线程可能存在的阻塞问题,从而提高时间上的效率。

ThreadLocal 有什么应用?

ThreadLocal的主要价值在于线程隔离,ThreadLocal中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。

ThreadLocal在线程隔离的最常用案例为:可以每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

比如可以给每个线程绑定一个数据库连接,那么这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。在Spring框架中,ThreadLocal用于存储数据库连接等线程特定的资源。由于数据库连接是线程不安全的,因此每个线程都需要有自己的连接副本。Spring通过ThreadLocal将数据库连接与当前线程关联起来,从而避免了多线程环境下的数据竞争和不一致问题。在Spring的事务管理中,ThreadLocal也扮演着重要角色。它确保了每个线程都有自己的事务上下文,包括事务状态、回滚点等信息,从而实现了事务的隔离性。

RPC

在远程过程调用(RPC)框架中,ThreadLocal用于存储和传递与当前调用相关的上下文信息。通过将这些信息存储在ThreadLocal中,无需在方法调用链中显式传递 request,简化代码逻辑,每个线程独享自己的 Request 实例,避免多线程竞争。若 RPC 框架支持异步编程(如 CompletableFuture、响应式编程),ThreadLocal 的值无法自动传递到子线程或回调链中。

@Slf4j
public class YrpcBootstrap {
    // 保存request对象,可以到当前线程中随时获取
    public static final ThreadLocal<YrpcRequest> REQUEST_THREAD_LOCAL = new ThreadLocal<>();
}

ThreadLocal 怎么实现的?

    public static void main(String[] args) {
        User user1 = new User("张三",1);
        User user2 = new User("李四",2);
        ThreadLocal<User> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<User> threadLocal2 = new ThreadLocal<>();
        threadLocal1.set(user1);
        threadLocal2.set(user2);
        User u1 = threadLocal1.get();
        User u2 = threadLocal2.get();
        System.out.println(u1);
        System.out.println(u2);
    }
输出:
User(name=张三, id=1)
User(name=李四, id=2)

当创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量,这样就相当于为每个线程维护了一个变量副本。

set方法

public void set(T value) {
	//1. 获取当前线程实例对象
    Thread t = Thread.currentThread();

	//2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    if (map != null)
	   //3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
       map.set(this, value);
    else
	  //4.map为null,则新建ThreadLocalMap并存入value
      createMap(t, value);
}

ThreadLocalMap 是怎样来的呢?通过getMap(t)

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

该方法直接返回当前线程对象 t 的一个成员变量 ThreadLocals:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap ThreadLocals = null;

再来看 set 方法,当 map 为 null 的时候会通过createMap(t,value)方法 new 出来一个:

void createMap(Thread t, T firstValue) {
    t.ThreadLocals = new ThreadLocalMap(this, firstValue);
}

该方法 new 了一个 ThreadLocalMap 实例对象,然后以当前 ThreadLocal 实例作为 key,值为 value 存放到 ThreadLocalMap 中,然后将当前线程对象的 ThreadLocals 赋值为 ThreadLocalMap 对象。

set 方法的重要性在于它确保了每个线程都有自己的变量副本。由于这些变量是存储在与线程关联的映射表中的,所以不同的线程之间的这些变量互不影响。

get方法

get 方法用于获取当前线程中 ThreadLocal 的变量值,同样的还是来看源码:

public T get() {
  //1. 获取当前线程的实例对象
  Thread t = Thread.currentThread();

  //2. 获取当前线程的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
	//3. 获取map中当前ThreadLocal实例为key的值的entry
    ThreadLocalMap.Entry e = map.getEntry(this);

    if (e != null) {
      @SuppressWarnings("unchecked")
	  //4. 当前entitiy不为null的话,就返回相应的值value
      T result = (T)e.value;
      return result;
    }
  }
  //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
  return setInitialValue();
}

代码逻辑请看注释;我们来看下 setInitialValue 主要做了些什么事情?

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);
    return value;
}

该方法的逻辑和 set 方法几乎一样,主要来看下 initialValue 方法:

protected T initialValue() {
    return null;
}

这个方法是通过 protected 修饰的,也就意味着 ThreadLocal 的子类可以重写该方法给一个合适的初始值

这里是 initialValue 方法的典型用法:

private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 0; // 初始值设置为0
    }
};

此代码段创建了一个新的 ThreadLocal 对象,其初始值为 0。任何尝试首次访问此 ThreadLocal 变量的线程都会看到值 0。

整个 setInitialValue 方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值。这种“懒惰”初始化的方法确保了仅在实际需要特定于线程的值时才创建这些值。

remove方法

public void remove() {
	//1. 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
 	if (m != null)
		//2. 从map中删除以当前ThreadLocal实例为key的键值对
		m.remove(this);
}

remove 方法的作用是从当前线程的 ThreadLocalMap 中删除与当前 ThreadLocal 实例关联的条目。这个方法在释放线程局部变量的资源或重置线程局部变量的值时特别有用。

以下是使用 remove 方法的示例代码:

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");

Thread thread = new Thread(() -> {
    System.out.println(threadLocal.get()); // 输出 "Initial Value"
    threadLocal.set("Updated Value");
    System.out.println(threadLocal.get()); // 输出 "Updated Value"
    threadLocal.remove();
    System.out.println(threadLocal.get()); // 输出 "Initial Value"
});
thread.start();

输出结果:

Initial Value
Updated Value
Initial Value

Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry.value 设置为 null,这样可以在很大程度上避免内存泄漏。

ThreadLocalMap 的源码分析

ThreadLocalMap 是 ThreadLocal 类的静态内部类,它是一个定制的哈希表,专门用于保存每个线程中的线程局部变量。

static class ThreadLocalMap {}

和大多数容器一样,ThreadLocalMap 内部维护了一个 Entry 类型的数组 类型的数组 table,长度为 2 的幂次方。

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

来看下 Entry 是什么:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry 继承了弱引用 WeakReference>,它的 value 字段用于存储与特定 ThreadLocal 对象关联的值。使用弱引用作为键允许垃圾收集器在不再需要的情况下回收 ThreadLocal 实例。

image-20250316213805361 image-20250316213821636

上图中的实线表示强引用,虚线表示弱引用。每个线程都可以通过 ThreadLocals 获取到 ThreadLocalMap,而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组。

当我们为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 Entry 往这个 ThreadLocalMap 中存放。注意,Entry 的 key 为弱引用,意味着当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。

这样一来,ThreadLocalMap 就会出现 key 为 null 的 Entry,也就没办法访问这些 key 对应的 value,如果线程迟迟不结束的话,这些 key 为 null 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,无法回收就会造成内存泄漏。

当然,如果 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收时都会被系统回收。但实际开发中,线程为了复用是不会主动结束的,比如说数据库连接池,过大的线程池可能会增加内存泄漏的风险,因此合理配置线程池的大小和线程的存活时间有助于减轻这个问题。

为了避免这个问题,在每次使用完 ThreadLocal 之后,最好明确调用 ThreadLocal 的 remove 方法来删除与当前线程关联的值。这样可以确保线程再次使用时不会存储旧的、不再需要的值。

ThreadLocalMap 怎么解决 Hash 冲突的?

开放定址法

如果计算得到的槽位 i 已经被占用,ThreadLocalMap 会采用开放地址法中的线性探测来寻找下一个空闲槽位:

如果 i 位置被占用,尝试 i+1。

如果 i+1 也被占用,继续探测 i+2,直到找到一个空位。

如果到达数组末尾,则回到数组头部,继续寻找空位。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

ThreadLocalMap 是使用开放地址法来处理哈希冲突的,和 HashMap 不同,之所以采用不同的方式主要是因为:ThreadLocalMap 中的哈希值分散的比较均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,冲突的概率就更小了。

set 方法

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;
	//根据ThreadLocal的hashCode确定Entry应该存放的位置 
	//在 len 为 2 的幂时,两者结果相同,位运算 & (len-1) 比取模运算 % len 更快,
    int i = key.ThreadLocalHashCode & (len-1);

	//采用开放地址法,hash冲突的时候使用线性探测
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//覆盖旧Entry
        if (k == key) {
            e.value = value;
            return;
        }
		//当key为null时,说明ThreadLocal强引用已经被释放掉,那么就无法
		//再通过这个key获取ThreadLocalMap中对应的entry,这里就存在内存泄漏的可能性
        if (k == null) {
			//用当前插入的值替换掉这个key为null的“脏”entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//新建entry并插入table中i处
    tab[i] = new Entry(key, value);
    int sz = ++size;
	//插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  0001 1010 0011 1011   (0x1A3B)
& 0000 0000 0000 1111   (15)
-------------------------------
  0000 0000 0000 1011   → 十进制 11

set 方法的关键部分请看注释,这里有几点需要注意:

ThreadLocal 的 hashcode
private final int ThreadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode =new AtomicInteger();
  /**
   * Returns the next hash code.
   */
  private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
  }

ThreadLocal 的 hashCode 是通过 nextHashCode() 方法获取的,该方法实际上是用 AtomicInteger 加上 0x61c88647 来实现的。0x61c88647 是一个魔数,用于 ThreadLocal 的哈希码递增。这个值的选择并不是随机的,它是一个质数。

ThreadLocalMap扩容机制是什么?

与 HashMap 不同,ThreadLocalMap 并不会直接在元素数量达到阈值时立即扩容,而是先清理被 GC 回收的 key,然后在填充率达到四分之三时进行扩容。

private void rehash() {
    // 清理被 GC 回收的 key
    expungeStaleEntries();

    //扩容
    if (size >= threshold - threshold / 4)
        resize();
}

清理过程会遍历整个数组,将 key 为 null 的 Entry 清除。

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        // 如果 key 为 null,清理 Entry
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

阈值 threshold 的默认值是数组长度的三分之二。

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

扩容时,会将数组长度翻倍,然后重新计算每个 Entry 的位置,采用线性探测法来寻找新的空位,然后将 Entry 放入新的数组中。

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; // 释放 Value,防止内存泄漏
            } else {
                // 重新计算位置
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    // 线性探测寻找新位置
                    h = nextIndex(h, newLen);
                }
                // 放入新数组
                newTab[h] = e;
                count++;
            }
        }
    }
    table = newTab;
    size = count;
    threshold = newLen * 2 / 3; // 重新计算扩容阈值
}

总结:ThreadLocalMap 采用的是“先清理再扩容”的策略,扩容时,数组长度翻倍,并重新计算索引,如果发生哈希冲突,采用线性探测法来解决。

三分恶面渣逆袭:ThreadLocalMap扩容

key为什么是弱引用?

key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。

弱引用会在每次GC时回收,无论内存是否足够。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理。

image-20250316214841861

父线程能用 ThreadLocal 给子线程传值吗?

不能。

二哥的 Java 进阶之路:子线程无法获取父线程的 ThreadLocal

因为 ThreadLocal 变量存储在每个线程的 ThreadLocalMap 中,而子线程不会继承父线程的 ThreadLocalMap。

可以使用 InheritableThreadLocal来解决这个问题。

二哥的 Java 进阶之路:InheritableThreadLocal源码

子线程在创建的时候会拷贝父线程的 InheritableThreadLocal 变量。

二哥的 Java 进阶之路:Thread 源码

来看一下使用示例:

class InheritableThreadLocalExample {
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("父线程的值");

        new Thread(() -> {
            System.out.println("子线程获取的值:" + inheritableThreadLocal.get()); // 继承了父线程的值
        }).start();
    }
}

在 Thread 类的定义中,每个线程都有两个 ThreadLocalMap:

public class Thread {
    /* 普通 ThreadLocal 变量存储的地方 */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* InheritableThreadLocal 变量存储的地方 */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

普通 ThreadLocal 变量存储在 threadLocals 中,不会被子线程继承。

InheritableThreadLocal 变量存储在 inheritableThreadLocals 中,当 new Thread() 创建一个子线程时,Thread 的 init() 方法会检查父线程是否有 inheritableThreadLocals,如果有,就会拷贝 InheritableThreadLocal 变量到子线程:

Javaprivate void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    // 获取当前父线程
    Thread parent = currentThread();
    // 复制 InheritableThreadLocal 变量
    if (parent.inheritableThreadLocals != null) {
        this.inheritableThreadLocals = 
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
}

为了解决value内存泄露问题, ThreadLocal 做了什么?

为了解决value内存泄露问题,Java 的 ThreadLocal 实现了两大清理方式:

  • 探测式清理(Proactive Cleanup)
  • 启发式清理(Heuristic Cleanup)

探测式清理

当线程调用 ThreadLocalget()set()remove()方法时,会探测式的去触发对 ThreadLocalMap 的清理。此时,ThreadLocalMap 会检查所有键(ThreadLocal 实例),并移除那些已经被垃圾回收的key键及其对应的value 值。这种清理是主动的,因为它是在每次操作 ThreadLocal 时进行的。

从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。

注意:这里把清理的开销放到了get、set操作上,如果get的时候无用Entry(Entry的Key为null)特别多,那这次get相对而言就比较慢了。

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) {
                    // 将k=null的entry置为null
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // k不为null,则rehash从新分配配置
                    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;
        }

启发式清理

ThreadLocalMap 的 set() 方法中,有一个阈值(默认为 ThreadLocalMap.Entry 数组长度的 1/4)。

当 ThreadLocalMap 中的 Entry 对象被删除(通过键的弱引用被垃圾回收)并且剩余的 Entry 数量大于这个阈值时,会触发一次启发式清理操作。这种清理是启发式的,因为它不是每次操作都进行,而是基于一定的条件和概率。

从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。

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.get() == null) {
                    n = len;
                    removed = true;
                    // 移除
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

但还是建议手动清楚解决内存泄漏,而内存泄漏通常发生在使用线程池的场景中,因为线程池中的线程通常是长期存在的,它们的ThreadLocal变量也不会自动清理,这可能导致内存泄漏。尽管有弱引用(解决key泄露)以及这些清理机制(解决value泄露),但最佳实践业务主动清理,业务上解决这个问题的一个方法是,每当使用完ThreadLocal变量后,显式地调用remove()方法来清除它,这样可以避免潜在的内存泄漏问题,并减少垃圾回收的压力。

为什么ThreadLocal推荐定义成final static,但这样定义又有什么问题?

编程规范:ThreadLocal 实例作为ThreadLocalMap的Key,针对一个线程内所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。

由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用 static修饰ThreadLocal 就会节约内存空间。另外,为了确保ThreadLocal 实例的唯一性,除了使用static修饰之外,还会使用final进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:

 //推荐使用static final线程本地变量
 private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();

以上代码,为什么ThreadLocal实例除了添加static final 修饰之后,还常常加上了 private修饰呢?

主要目的是 缩小使用的范围,尽可能不让他人引用。

但使用static 、final修饰ThreadLocal实例也会带来副作用: 内存泄漏。

由于使用static 、final修饰TheadLocal对象实例, 导致了这个被 ThreadLocalMap中Entry的Key所引用的ThreadLocal对象实例,一直存在强引用。这里有一个严重后果,这个 使用static 、final修饰TheadLocal对象实例 一直不会被GC,会一直存在。

使用static 、final修饰ThreadLocal实例,导致了两个问题:

  • 问题1:导致JDK解决key内存泄露问题的弱引用清理方式彻底失效。

  • 问题2:导致JDK解决value内存泄露问题的两大清理方式彻底失效。

    • 探测式清理(Proactive Cleanup) 彻底失效
    • 启发式清理(Heuristic Cleanup)彻底失效 。

image-20250320103058646

这使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会导致Entry中的Value指向的对象一直存在强引用,Value指向的对象在线程生命期内不会被释放,最终导致内存泄露。

所以,使用static 、final修饰TheadLocal实例,使用完后必须使用remove()进行手动释放。

如果使用线程池,可以定制线程池的afterExecute方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用TheadLocal实例的remove()方法对其手动释放,从而实现的其线程内部的Entry得到释放,参考的代码如下:

    //线程本地变量,用于记录线程异步任务的开始执行时间
  private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
     ExecutorService pool = new ThreadPoolExecutor(2,
            4, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
           ...省略其他        
            //异步任务执行完成之后的钩子方法
            @Override
            protected void afterExecute(Runnable target, Throwable t)
            {
              ...省略其他        
                //清空TheadLocal实例的本地值
                startTime.remove();
            }
  };

ThreadLocal和synchronized之间的比较

ThreadLocal和synchronized在Java中都是用于处理多线程问题的机制,但它们之间存在一些关键的区别。

核心思想

  • ThreadLocal:其核心思想是以空间换时间。它为每个线程提供了一个独立的变量副本,使得每个线程都可以访问和修改自己的变量副本,而不会影响到其他线程。由于每个线程操作的是自己的变量副本,因此多个线程可以同时访问该变量,且相互之间不会产生影响。这种机制主要用于保存线程私有数据、提高性能、管理线程特定的资源等场景。
  • synchronized:其核心思想是以时间换空间。它确保同一时刻只有一个线程能够执行被synchronized修饰的代码块或方法,其他线程必须等待锁的释放。多个线程访问的是同一个变量,当多个线程同时访问该变量时,需要抢占锁,并且等待获取锁的线程释放锁,因此会消耗较多的时间。synchronized主要用于保护共享资源,防止竞态条件和数据不一致问题。

应用场景

  • ThreadLocal主要用于线程间的数据隔离,常见应用场景包括线程封闭(将对象封闭在单个线程中,避免线程安全问题)、保存线程上下文信息(如在Web开发中存储用户信息和请求参数)、数据库连接管理(确保每个线程获取到自己的数据库连接)以及线程池中的任务隔离等。
  • Synchronized主要用于线程间的数据共享,常用于保护共享资源,如共享数据或对象,确保同一时间只有一个线程访问这些资源。此外,它还可以用于保护需要原子性执行的代码块,防止多线程并发执行导致的问题。

性能和资源消耗

综上所述

  • ThreadLocal为每个线程创建变量副本,因此会消耗较多的内存。但由于线程间互不干扰,所以并发性能较高。
  • synchronized则通过锁机制来控制线程对共享资源的访问,虽然节省了内存空间,但在多线程环境下可能会因为锁竞争而降低性能。

了解Netty中的FastThreadLocal吗?

FastThreadLocal 对 ThreadLocal 的升级与优化

FastThreadLocal 是 Netty 针对传统 ThreadLocal 在高并发场景下的性能瓶颈和内存问题进行深度优化的产物,其核心改进在于 通过数组索引替代哈希表、避免哈希冲突、提升访问速度,并简化内存管理


1. 传统 ThreadLocal 的瓶颈

(1) 哈希表性能问题

哈希冲突ThreadLocalMap 使用线性探测法解决冲突,当哈希表中元素较多时,set()/get() 的时间复杂度可能退化到 (O(n))。
扩容开销:哈希表扩容需要重新计算所有键的哈希位置,耗时较高。

(2) 内存泄漏风险

弱引用键Entrykey 是弱引用,但 value 是强引用。若未及时调用 remove(),线程池中的线程可能长期持有无效 Entry,导致内存泄漏。


2. FastThreadLocal 的核心升级

(1) 数据结构:数组替代哈希表

存储方式:每个线程维护一个 Object[] 数组,FastThreadLocal 实例在初始化时分配一个 唯一索引,通过索引直接访问数组元素。

// Netty 的 FastThreadLocal 核心代码
public class FastThreadLocal<V> {
    private final int index; // 唯一索引

    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

    public V get() {
        Object[] array = InternalThreadLocalMap.get().array;
        return (V) array[index];
    }
}

访问速度:直接通过数组下标 (O(1)) 访问,无哈希冲突。

// 传统 ThreadLocal 的哈希计算
int i = key.threadLocalHashCode & (len - 1);
(2) 索引预分配

索引生成:所有 FastThreadLocal 实例在初始化时分配全局唯一的递增索引,确保不同实例的数组位置独立。

// Netty 索引分配逻辑
public static int nextVariableIndex() {
    int index = nextIndex.getAndIncrement();
    if (index < 0) {
        throw new IllegalStateException("Too many thread-local indexed variables");
    }
    return index;
}
(3) 内存管理优化

数组扩容策略:按需扩容,每次扩容至当前长度的 2 倍,避免频繁扩容。
自动清理机制:线程结束时自动释放数组内存,减少内存泄漏风险。


3. 性能对比

(1) 读写速度
操作ThreadLocalFastThreadLocal
get()/set()依赖哈希表,有冲突风险直接数组索引 (O(1))
扩容开销低(预分配索引)
(2) 内存占用
指标ThreadLocalFastThreadLocal
Entry 对象数量每个 ThreadLocal 对应一个 Entry全局数组,无 Entry 对象
内存碎片高(哈希表结构)低(连续数组)

5. 代码示例

(1) 传统 ThreadLocal 使用
ThreadLocal<String> userHolder = new ThreadLocal<>();
userHolder.set("Alice");
String user = userHolder.get();
userHolder.remove();
(2) FastThreadLocal 使用
FastThreadLocal<String> fastUserHolder = new FastThreadLocal<>();
fastUserHolder.set("Bob");
String user = fastUserHolder.get();
// 无需手动 remove,线程退出时自动清理

7. 总结

优化维度ThreadLocalFastThreadLocal
数据结构哈希表(线性探测法)数组(直接索引)
性能有哈希冲突风险稳定 (O(1)),无冲突
内存管理需手动清理 Entry自动清理数组元素
适用场景通用线程本地存储高并发、低延迟、大规模线程池环境

FastThreadLocal 通过 数组索引化存储预分配机制,在性能和内存管理上超越传统 ThreadLocal

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马走日mazouri

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值