ThreadLocal

1. ThreadLocal是什么

ThreadLocal 是 java.lang 下面的一个类,是用来解决 java 多线程程序中并发问题的一种途径;通过为每一个线程创建一份共享变量的副本来保证各个线程之间的变量的访问和修改互相不影响ThreadLocal 绕过了线程间如何竞争一个变量的惯常思路,以用空间换时间的思想从另一个角度解决了线程安全问题。

简单来说就像“不是大家一起用一个大锅做饭(共享变量)”,而是“每个线程自己带个小锅(副本)单独做饭”。这样就不会出现谁抢谁饭、谁把锅弄脏的问题 —— 也就是避免了多线程之间的数据竞争和同步问题。


2. ThreadlLocal内存布局

首先是 Thread 对象,为了协助 ThreadLocal,Thread 对象在它的内部专门创建了一个 ThreadLocal.ThreadLocalMap 对象,源码就是:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 是 ThreadLocal 的一个内部类,是一个类 Map 的实现,类 Map 是指它虽然提供了类似 Map 的操作,但没有实现 Map 接口;ThreadLocalMap 用数组存储数据,没有像 HashMap 等其他 Map 结构一样,用链表 + 数组的方式实现。ThreadLocalMap 包含了一个 Entry 数组,每个 Entry 对象包含一个 ThreadLocal 变量的引用和一个对应的值。

在 ThreadLocalMap 中,ThreadLocal 变量的引用被用作哈希表的键,而对应的值的引用则被存储在 Entry 对象中。由于每个线程都有自己独立的 ThreadLocalMap 对象,因此不同线程中的 ThreadLocal 变量是独立的,不会相互干扰。

当一个线程创建一个 ThreadLocal 变量时,它会在自己的 ThreadLocalMap 中添加一个 Entry 对象,其中 ThreadLocal 变量的引用被用作键,而对应的变量的引用则被存储在 Entry 对象的 Value 中。当线程需要访问 ThreadLocal 变量时,它会从自己的 ThreadLocalMap 中获取对应的值。


3. ThreadlLocal的使用

3.1 成员方法

方法名具体用法

T initialValue()

返回此线程局部变量的初始值

T get()

返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。

set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。

remove()

移除此线程局部变量的值。

3.2 源码分析

set()方法

/**
 * 为当前线程设置这个 ThreadLocal 变量的值。
 * 一般情况下,子类不需要重写这个方法,只需依赖 {@link #initialValue} 方法来初始化值即可。
 * @param value 要设置到当前线程中的值(即该 ThreadLocal 的副本值)
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取该线程对应的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果 map 已存在(说明之前该线程已经使用过 ThreadLocal)
    if (map != null)
        // 将当前 ThreadLocal 实例作为 key,把 value 设置进去
        map.set(this, value);
    else
        // 当前线程是第一次使用 ThreadLocal,创建新的 ThreadLocalMap 并设置值
        createMap(t, value);
}

get()方法

/**
 * 获取当前线程中此 ThreadLocal 变量对应的值。
 * 
 * 如果当前线程还未设置该变量的值(即首次访问),
 * 那么会调用 initialValue() 方法来初始化,并将其保存到当前线程的 ThreadLocalMap 中。
 *
 * @return 当前线程的该 ThreadLocal 变量对应的值
 */
public T get() {
    // 获取当前正在执行的线程
    Thread t = Thread.currentThread();
    // 获取该线程对应的 ThreadLocalMap(用于存储该线程的所有 ThreadLocal 变量)
    ThreadLocalMap map = getMap(t);
    // 如果 ThreadLocalMap 不为空
    if (map != null) {
        // 尝试从 map 中获取以当前 ThreadLocal 实例为 key 的 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果找到了对应的 Entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;  // 获取存储的值,并进行类型转换
            return result;          // 返回该值
        }
    }
    // 如果当前线程还未存储该 ThreadLocal 的值,则调用 setInitialValue() 方法:
    // 1. 初始化值(通过 initialValue());
    // 2. 保存到 map 中;
    // 3. 返回这个初始化值。
    return setInitialValue();
}

remove()方法

/**
 * 移除当前线程中当前 ThreadLocal 对象所关联的值。
 * 
 * 如果当前线程之后再次调用 get() 方法访问该 ThreadLocal 变量,
 * 那么该变量将会通过 initialValue() 方法重新初始化,
 * 除非在此之前,调用了 set() 方法设置了新值。
 * 
 * 也就是说:调用 remove() 后,再次 get() 会触发重新初始化逻辑。
 * 注意:这可能导致 initialValue() 被多次调用!
 * 
 * 使用 remove() 的目的主要是为了避免内存泄漏,特别是在使用线程池时。
 * @since 1.5
 */
public void remove() {
    // 获取当前线程所持有的 ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果当前线程的 ThreadLocalMap 存在
    if (m != null)
        // 在 Map 中移除以当前 ThreadLocal 实例为 key 的条目
        m.remove(this);
}

内部类

// ThreadLocalMap 是 ThreadLocal 的内部静态类,用于存储线程本地变量的键值对。
static class ThreadLocalMap {
    /**
     * Entry 是 ThreadLocalMap 的静态内部类。
     * 它继承自 WeakReference,表示 key 是 ThreadLocal 的弱引用。
     * 
     * 为什么使用弱引用?
     * 为了防止内存泄漏:如果外部没有强引用指向这个 ThreadLocal 对象了,
     * 那么它可以被 GC 回收。而如果这里使用强引用,就会导致 ThreadLocal 对象无法被释放。
     * 
     * 一旦 WeakReference.get() 返回 null,说明该 ThreadLocal 实例已被回收,
     * 这种 Entry 被称为“过期的条目”(stale entries),可以从表中清除。
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** 与该 ThreadLocal 对应存储的值,是一个强引用对象。 */
        Object value;
        /**
         * 构造方法。
         * 
         * @param k ThreadLocal 实例(key)
         * @param v 要保存的值
         */
        Entry(ThreadLocal<?> k, Object v) {
            // 用 ThreadLocal 对象 k 初始化弱引用
            super(k);
            // 保存实际的值 v(强引用)
            value = v;
        }
    }
}


4. ThreadLocal内存泄漏

引用类型:

强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。

软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。

弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。

虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

ThreadLocalMap的key就是ThreadLocal对象,他有两个引用源,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中的Key对他的引用。

而对于value来说,他的引用就一条,就是从Thread对象过来的。 

4.1 造成内存泄露的原因

(一)key 的泄漏


栈上的ThreadLocal Ref引用不再使用了,即当前方法结束处理后,这个对象引用就不再使用了,
那么,ThreadLocal对象因为还有一条引用链存在,如果是强引用的话,这里就会导致ThreadLocal对象无法被回收,可能导致 OOM。

(二)value 的泄漏
假设我们使用了线程池,如果 Thread 对象一直被占用使用中( 如在线程池中被重复使用 ),但是此时我们的 ThreadLocalMap 生命周期和 Thread 的一样,它不会回收,这时候就出现了一个现象。
这就意味着,Value 这条引用链就一直存在,那么就会导致 ThreadLocalMap 无法被JVM回收,可能导致 OOM。

4.2 解决方式

ThreadLocalMap 底层使用数组来保存元素,使用“线性探测法”来解决hash冲突的,在每次调用ThreadLocal 的 get、set、remove 等方法的时候,内部会实际调用 ThreadLocalMap 的get、set、remove等操作。

而 ThreadLocalMap 的每次 get、set、remove,都会清理 key 为 null,但是 value 还存在的 Entry。

所以,当我们在一个ThreadLocal用完之后,手动调用一下remove,就可以在下一次GC的时候,把Entry清理掉。

5. ThreadLocal的使用场景

(1)解决并发问题,这个不需要多说了。

(2)在线程中传递数据,在同一个线程执行过程中,ThreadLocal的数据一直在,所以我们可以在前面把数据放到ThreadLocal中,然后再后面的时候再取出来用,就可以避免要把这些数据一直通过参数传递。

一些具体用途:

1. 用户身份信息存储: 在很多应用中,都需要做登录鉴权,一旦鉴权通过之后,就可以把用户信息存储在ThreadLocal中,这样在后续的所有流程中,需要获取用户信息的,直接取ThreadLocal中获取就行了。非常的方便。

2. 线程安全:ThreadLocal可以用来定义一些需要并发安全处理的成员变量,比如SimpleDateFormat,由于 SimpleDateFormat 不是线程安全的,可以使用 ThreadLocal 为每个线程创建一个独立的 SimpleDateFormat 实例,从而避免线程安全问题。

3. 日志上下文存储:在Log4j等日志框架中,经常使用ThreadLocal来存储与当前线程相关的日志上下文。这允许开发者在日志消息中包含特定于线程的信息,如用户ID或事务ID,这对于调试和监控是非常有用的。

4. 数据库Session:很多ORM框架,如Hibernate、Mybatis,都是使用ThreadLocal来存储和管理数据库会话的。这样可以确保每个线程都有自己的会话实例,避免了在多线程环境中出现的线程安全问题。

5. PageHelper分页:PageHelper是MyBatis中提供的分页插件,主要是用来做物理分页的。我们在代码中设置的分页参数信息,页码和页大小等信息都会存储在ThreadLocal中,方便在执行分页时读取这些数据。

之后引入TTL框架

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值