探究ThreadLocal线程局部变量

1.ThreadLocal有什么用

ThreadLocal的主要作用是​​为每个线程提供独立的变量副本​​,实现线程隔离。这里副本其实相当于用的不是实例,而是拷贝的意思;
看一段代码再来理解一下:

public class ThreadLocal1 {


        private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
        private static final ThreadLocal<Integer> countThreadLocal = new ThreadLocal<>();

        public static void main(String[] args) {
            // 线程1
            new Thread(() -> {
                userThreadLocal.set("User1");
                countThreadLocal.set(100);
                System.out.println(userThreadLocal.get());  // 输出 User1
                System.out.println(countThreadLocal.get()); // 输出 100
            }).start();

            // 线程2
            new Thread(() -> {
                userThreadLocal.set("User2");
                countThreadLocal.set(200);
                System.out.println(userThreadLocal.get());  // 输出 User2
                System.out.println(countThreadLocal.get()); // 输出 200
            }).start();
        }

}

这里声明了两个ThreadLocal,然后分别在两个线程使用;为了理解变量副本,把第二个Thread userThreadLocal.set("User2");注释掉,输出为null;

 // 线程2
      new Thread(() -> {
             //userThreadLocal.set("User2");
             countThreadLocal.set(200);
             System.out.println(userThreadLocal.get());  // 输出 null
             System.out.println(countThreadLocal.get()); // 输出 200
         }).start();

可能有的人会说因为线程1并没有对userThreadLocal赋值,其实并不是,最后输出如下:

User1
100
null
200

其实就是因为每个线程的ThreadLocal是独立的

2.ThreadLcoal原理

https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html#%E2%AD%90%EF%B8%8Fthreadlocal-%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97

为了解释上面的结果,接下来从Thread,ThreadLocal源码来解释;
ThreadLocal.java

public ThreadLocal() {
    }

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }
        public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
        void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
        ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    ThreadLocal.ThreadLocalMap threadLocals = null;

set方法:
可以看到是通过获取当前线程然后通过当前线程t获取ThreadLocalMap,从这里就可以知道最终存取变量的是Thread中的ThreadLocalMap变量;
接下来看一下ThreadLocalMap的源码:

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

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

也就是说最终使用Entry(用来存取键值对),

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

看下面一幅图就知道了:
在这里插入图片描述
ThreadLocal最终是属于拥有他的线程ThreadLocalMap;

3.优势是什么

3.1. ​​实现真正的线程隔离​​

ThreadLocal的核心目的是让每个线程拥有自己独立的变量副本。通过将数据存储在Thread对象内部的ThreadLocalMap中,实现了:

线程A访问ThreadLocal时,自动从线程A自己的ThreadLocalMap中获取数据
线程B访问同一个ThreadLocal时,从线程B自己的ThreadLocalMap获取数据
两个线程互不干扰,天然线程安全

3.2. ​​避免竞争开销​​

如果采用全局哈希表(比如用ConcurrentHashMap存储所有线程的ThreadLocal数据):
Map<Thread, Map<ThreadLocal, Object>> globalMap
每次访问都需要:

获取当前线程对象
通过线程对象查二级Map
还需要处理并发问题(锁竞争)

而现在的设计:
1.每个线程直接持有一个ThreadLocalMap
2.get/set操作只需要访问当前线程的局部变量(无锁)
3.性能接近直接访问普通变量

3.3 ​​与线程生命周期绑定​​

ThreadLocalMap的生命周期与Thread绑定:

线程终止时,ThreadLocalMap会被自动回收
避免了内存泄漏管理复杂度(虽然仍需注意弱引用问题)
天然符合"线程局部变量"的语义
4. ​​数据结构优化​​
ThreadLocalMap是专门优化的定制哈希表:
使用开放寻址法而非链地址法(更适合少量数据)
Key(ThreadLocal对象)使用弱引用,避免内存泄漏
自动清理失效Entry(set/get时触发探测式清理)

4.使用

实际应用场景示例

4.1. 日期格式化(SimpleDateFormat线程安全使用)

public class DateFormatter {
    // 非线程安全用法
    // private static SimpleDateFormat unsafeFormat = new SimpleDateFormat("yyyy-MM-dd");
    
    // 线程安全用法
    private static final ThreadLocal<SimpleDateFormat> safeFormat = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    
    public static String format(Date date) {
        return safeFormat.get().format(date);
    }
}

4.2. 用户上下文传递(Web应用)

public class UserContextHolder {
    private static final ThreadLocal<User> context = new ThreadLocal<>();
    
    public static void setUser(User user) {
        context.set(user);
    }
    
    public static User getUser() {
        return context.get();
    }
    
    public static void clear() {
        context.remove();
    }
}

// 在拦截器中设置用户信息
class AuthInterceptor {
    public boolean preHandle(HttpServletRequest request) {
        User user = getUserFromRequest(request);
        UserContextHolder.setUser(user);
        return true;
    }
}

// 在业务层直接获取
class OrderService {
    public void createOrder() {
        User currentUser = UserContextHolder.getUser();
        // 使用用户信息...
    }
}

5.内存泄露问题

5.1什么是内存泄露

内存泄漏指的是​​程序申请的内存没有被正确释放​​,导致这部分内存无法被再次使用,最终可能引发 ​​OOM(OutOfMemoryError)​​。

ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024 * 1024]);  // 存 1MB 数据

// 假设不再使用 threadLocal,但没有 remove()
threadLocal = null;  // ThreadLocal 对象被回收(key=null),但 value 还在!

5.2ThreadLocal的内存泄露

本质是由于他的内部设计导致的;

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // 关键点:调用父类WeakReference的构造方法
        value = v;
    }
}

这里ThreadLocal作为k,调用了父类的构造方法;
造成内存泄露的本质两点原因:

  1. 用于存储的Entry中的key值是ThreadLocal弱引用。这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。
    对于弱引用会被解释为:

弱引用是 Java 中的一种引用类型,它比普通的强引用(Strong Reference)更弱,​​不会阻止垃圾回收器(GC)回收对象​​。当一个对象​​仅被弱引用指向​​(没有强引用指向它)时,垃圾回收器会在下一次 GC 时自动回收该对象,即使程序仍然持有这个弱引用。

可以看看下面这段代码来理解:

    public static void main(String[] args) {
        Object strongRef = new Object();  // 强引用
        System.gc();                      // 触发 GC
        System.out.println(strongRef);    // 仍然能打印对象,因为强引用存在
        WeakReference<Object> weakRef = new WeakReference<>(new Object());
        System.gc();                      // 触发 GC
        System.out.println(weakRef.get()); // 可能输出 null,因为只有弱引用
    }
    //下面是 WeakReference.java
   
       public T get() {
        return this.referent;
    }

其中WeakReference.get()返回的就是当前引用,可以看到弱引用直接在gc回收直接变为null了,而强引用不会被回收(除非显示的置为null);

  1. 然后value是强引用,即使key被GC回收为null,但value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。
    所以,当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。

也就是说,内存泄漏的发生需要同时满足两个条件:

  • ThreadLocal 实例不再被强引用;
  • 线程持续存活,导致 ThreadLocalMap 长期存在。虽然 ThreadLocalMap 在 get(), set() 和 remove() 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。

5.3如何避免内存泄漏的发生?

  • 在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()。
    ThreadLocal
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocalMap

/**
 * 移除指定 ThreadLocal 键对应的 Entry
 * @param key 要移除的 ThreadLocal 键
 */
private void remove(ThreadLocal<?> key) {
    // 获取哈希表数组和长度
    Entry[] tab = table;
    int len = tab.length;
    
    // 计算初始哈希槽位(通过哈希码取模)
    int i = key.threadLocalHashCode & (len-1);  // 等价于 key.hashCode() % len
    
    // 遍历可能发生哈希冲突的槽位(开放寻址法)
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 检查当前 Entry 的 key 是否匹配
        if (e.get() == key) {
            // 1. 清除弱引用(将 key 置为 null)
            e.clear();  
            
            // 2. 清理 stale entry 并重新哈希后续元素
            expungeStaleEntry(i);
            
            // 3. 直接返回(不继续遍历)
            return;
        }
    }
    // 若未找到匹配的 key,直接结束(无操作)
}
  • 在线程池等线程复用的场景下,使用 try-finally 块可以确保即使发生异常,remove() 方法也一定会被执行。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值