什么是ThreadLocal,一篇文章彻底搞懂Java多线程中的ThreadLocal【保姆级教程】

本文将详细介绍Java并发编程(多线程)中常见的 ThreadLocal 类以及如何去使用它。

ThreadLocal类

1、ThreadLocal 是 Java 中提供的一种机制,用于为每个线程提供独立的变量副本,确保每个线程都有自己的变量,而不会和其他线程共享这些变量(即每个线程都有独立的存储空间,线程之间互不干扰。)

用途:主要用于解决多线程并发访问时的线程安全问题,尤其是在不想使用复杂的锁机制时,它是非常有效的工具。

2、创建方式:

2.1 最基础的:

ThreadLocal tl = new ThreadLocal();

这时:

  • 没有初始值;

  • 第一次调用 get() 会返回 null(除非重写 initialValue()

    ThreadLocal<String> local = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "默认值";
        }
    };
    

2.2 推荐方式:这个本质上也是创建一个 ThreadLocal 实例。

ThreadLocal<String> local = ThreadLocal.withInitial(() -> "默认值");

3、常用方法:

get()

  • 获取当前线程的 ThreadLocal 变量值。
  • 如果该线程是第一次调用 get(),则会返回通过 initialValue()withInitial() 方法指定的初始值。

set(T value)

  • 设置当前线程的 ThreadLocal 变量值。

remove()

  • 移除当前线程的 ThreadLocal 变量,清除与线程绑定的值,释放内存。

initialValue()

  • 为当前线程提供初始值,当线程第一次调用 get() 时,如果没有通过 set() 设置值,就会调用 initialValue()

withInitial(Supplier<? extends T>)

  • Java 8 引入的工厂方法,用来简化 ThreadLocal 的初始化过程。它接受一个 Supplier 接口,提供初始化值。

4、示例:

public class Test {

    // 创建一个 ThreadLocal 用于存储每个线程的账户余额:初始余额 100
    private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(() -> 100); 

    public void withdraw(int amount) {
        balance.set(balance.get() - amount); // 每个线程更新自己的余额
    }

    public int getBalance() {
        return balance.get(); // 获取当前线程的余额
    }

    public static void main(String[] args) {
        BankAccount account = new BankAccount();

        // 启动两个线程,分别操作账户
        Thread t1 = new Thread(() -> {
            account.withdraw(50);
            System.out.println(account.getBalance()); // 输出: 50
        });

        Thread t2 = new Thread(() -> {
            account.withdraw(30);
            System.out.println(account.getBalance()); // 输出: 70
        });

        t1.start();
        t2.start();
    }
}

注意:ThreadLocal 使用不当可能会导致内存泄漏。因为线程池中的线程通常是复用的,而 ThreadLocal 绑定的是线程私有数据。如果不调用 remove() 方法清除 ThreadLocal 变量,可能会导致线程长期持有该变量,无法释放内存。因此,建议在使用完 ThreadLocal 后调用 remove() 方法来清理资源。


ThreadLocal底层原理

每个线程 Thread 内部有一个 ThreadLocalMap ,键是 ThreadLocal 对象本身,值是当前线程中存储的变量副本:

// ThreadLocalMap 的构造函数(源码:)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY]; // 默认容量是16
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
}
Thread.currentThread()
    └── threadLocals(ThreadLocalMap)
            └── table[i] = Entry(WeakReference<ThreadLocalA>, valueA)
    		└── table[i+1] = Entry(WeakReference<ThreadLocalB>, valueB)

简单流程:

ThreadLocal<String> local = new ThreadLocal<>();
local.set("abc");

String value = local.get();
  • 调用 local.set(value),其实是:
    1. 获取当前线程(Thread.currentThread());
    2. 拿到当前线程的 ThreadLocalMap (也就是 Thread.threadLocals),没有就创建;
    3. 以 local 为 key,value 为你传的对象,存进 ThreadLocalMap
  • 调用 local.get(),其实是:
    1. 当前线程找自己的 ThreadLocalMap
    2. 拿 local 这个 key 对应的 value

每个线程互不影响,不用加锁也线程安全


1. ThreadLocalMap是什么

1、ThreadLocalMapThreadLocal 的一个静态内部类,它不是 Java 标准库中的 Map 接口的实现(如 HashMap 或 ConcurrentHashMap),而是专 为 ThreadLocal 服务的一种自定义轻量级 Map。

注意: 但 ThreadLocalMap 的实例却保存在 Thread 对象里,可以看 Thread 源码:

// Thread.java(源码简化)
public class Thread implements Runnable {
    // 每个线程拥有一个存储 ThreadLocal 数据的 map(也就是ThreadLocalMap)
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

2、怎么获取 ThreadLocalMap?以 ThreadLocal.set() 为例。从源码角度:看是如何获取 ThreadLocalMap

① 看 ThreadLocal.set() 源码:

// ThreadLocal.java(源码简化)
public class ThreadLocal<T> {
    public void set(T value) {
        // 1.获取当前线程
        Thread t = Thread.currentThread();
        
        // 2.从线程中拿到threadLoals
        ThreadLocalMap map = getMap(t);
        
        // 3.如果有map,往里存;如果没有map,就创建
        if (map != null) {
            // this 指的是当前 ThreadLocal 实例!
            map.set(this, value);
        } else {
            createMap(t, value);
        }
	}
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
    	t.threadLocals = new ThreadLocalMap(this, firstValue);
	}
}

② 看 ThreadLocal.get() 源码:

// ThreadLocal.java(源码简化)
public class ThreadLocal<T> {
    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;
            }
        }
        // 如果没有值,调用initialValue()
        return setInitialValue();
    }
}

2. 为什么ThreadLocalMap的key是弱引用
什么是弱引用

1、什么是弱引用?

类型是否被 GC举例
强引用(StrongReference)只要有引用,就不能被 GC正常的 new 出来的对象
弱引用(WeakReference)如果没有强引用,GC 就会清理new WeakReference<>(obj)

示例:

public class LeakDemo {
    public static void main(String[] args) {
        // 1.创建ThreadLocal并设置值,但不保留引用
        new ThreadLocal<>().set(new byte[1024*1024*10]); // 10MB
        
        // 2.主线程结束后,我们期待GC回收内存
        System.gc();
    }
}

这个过程内存中发生了什么?---->

① 创建 ThreadLocal,并执行 set()

new ThreadLocal<>().set(...)

这一步发生了两件事:

  • 创建了一个 ThreadLocal 实例(假设地址为 0x1001),但你没有变量保存这个实例
  • 调用了 .set(),当前线程的 Thread.threadLocals 中,添加了这样一个 Entry:
Entry:
    key   = new WeakReference<ThreadLocal@0x1001>
    value = new byte[10MB]

② 你没有保存 ThreadLocal 的引用

也就是说,只有 Thread 的 ThreadLocalMap 还保存了它的引用(key 是弱引用),其他地方都没有了。

此时 Java 的垃圾回收行为是:

  • 发现 ThreadLocal@0x1001 没有强引用了
  • 弱引用(WeakReference)不会阻止回收
  • 所以下一次 GC 时,key 被清除了(key == null)

但是:

③ value 是强引用,仍然在内存中

虽然 key 被 GC 清掉了,但 value(10MB 的 byte[])是正常的强引用:

Entry:
    key = null       ❌ 被 GC 回收了
    value = 10MB     ✅ 还在!

这时候你已经再也拿不到这个 10MB 的 value,但 JVM 还留着它,不会回收!----> 可能导致的问题:

这就叫“内存泄漏”:你无法访问这块内存,但它还在 JVM 中,无法释放 —— 泄漏了。如果是线程池线程(长时间不销毁),这种垃圾会越来越多,最终导致内存溢出(OutOfMemoryError)。


为什么要用弱引用

2、那为什么要用弱引用?

你可能会问:那为啥不让 key 是强引用,就不会 key == null 了?----> 错误!如果是强引用,GC 永远不会清理这个 ThreadLocal 对象,即使你已经不想用了。这就变成:

你用完了 ThreadLocal,但 JVM 还一直“死抱着”它 ----> 也泄漏。

所以用弱引用是对的,只不过你用完后得自己把 value 给删掉!

也就是:ThreadLocalMap 的 key 是弱引用是为了防止强引用无法 GC,但 value 是强引用,如果你不手动 remove(),value 就永远留在内存中,造成泄漏。

// 避免泄漏
try {
    ThreadLocalHolder.set("value");
    // 业务逻辑
} finally {
    ThreadLocalHolder.remove(); // 清理掉 Entry
}

这样就会把 ThreadLocalMap 中的 Entry 移除,不会残留 value



问题:如果我手里有 ThreadLocal 的变量强引用(如下代码),那 ThreadLocalMap 中的 key 是不是强引用?----> 不是

ThreadLocal<String> local = new ThreadLocal<>();
local.set("abc");
  • ThreadLocalMap 中的 key 永远是弱引用。哪怕你手里保存了 ThreadLocal 实例(比如变量 local
  • 只是这个变量保护了 ThreadLocal 对象不被 GC。但是在 ThreadLocalMap 中,它仍然是通过 WeakReference 被引用!

那手里有变量的强引用,有什么用?

  • ThreadLocal 不会被 GC(被你引用着)
  • 所以 ThreadLocalMap 的 key 仍然是弱引用,但不会被 GC 自动清除
  • 一旦你手里的变量被设置为 null,GC 就可以清除 ThreadLocalMap 的 key

至此,文章结束了,记得小赞赞走起,主页还有更多精彩内容!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小学鸡!

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

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

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

打赏作者

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

抵扣说明:

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

余额充值