ThreadLocal详解

文章目录

ThreadLocal

ThreadLocal文档注释:

text 复制代码
This class provides thread-local variables.
These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own,
independently initialized copy of the variable. 

文档大意:这个类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问它们的线程(通过其get方法或set方法)都有自己的独立初始化的变量副本。

如文档注释所说,ThraedLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。每个访问ThreadLocal变量的线程都有自己的隔离副本,这样防止了线程之间的干扰,消除了同步的需要。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中"Local"所要表达的意思。说白了ThreadLocal就是存放线程的局部变量的。

对比线程同步

ThreadLocal是修饰变量的,重点是在控制变量的作用域,初衷不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便使用。很多开发语言在语言级别都提供这种作用域的变量类型。

其实要保证线程安全,并不一定就是要进行同步,两者没有因果关系。同步只是保证共享数据竞争时的手段。如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性。线程安全,并不一定就是要进行同步,ThreadLocal目的是线程安全,但不是同步手段。

ThreadLocal和线程同步机制都可以解决多线程中共享变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。使用同步机制要求程序谨慎地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal

虽然ThreadLocal能够保证多线程访问数据安全,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间换时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

使用示例

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)T get()

ThreadLocal中主要有三个方法:

  • set():设置当前线程的线程局部变量的值。
  • get():该方法返回当前线程所对应的线程局部变量。
  • remove():删除当前线程的线程局部变量,目的是为了减少内存的占用。
java 复制代码
public class ThreadLocalExample {
    // 创建一个 ThreadLocal 变量,用于存储每个线程独立的值
    private static final ThreadLocal<String> threadLocalValue = new ThreadLocal<>();

    public static void main(String[] args) {

        Runnable task1 = () -> {
            // 设置线程局部变量的值
            threadLocalValue.set("Thread-1's Value");
            // 获取并打印线程局部变量的值
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
            // 删除线程局部变量的值
            threadLocalValue.remove();
            System.out.println(Thread.currentThread().getName() + " after remove: " + threadLocalValue.get());
        };

        Runnable task2 = () -> {
            // 设置线程局部变量的值
            threadLocalValue.set("Thread-2's Value");
            // 获取并打印线程局部变量的值
            System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
            // 删除线程局部变量的值
            threadLocalValue.remove();
            System.out.println(Thread.currentThread().getName() + " after remove: " + threadLocalValue.get());
        };

        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        thread1.start();
        thread2.start();
    }
}

除此之外,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值。

java 复制代码
public class ThreadLocalWithInitialExample {

    // 使用 withInitial 方法提供初始值
    private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public static void main(String[] args) {

        Runnable task1 = () -> {
            // 获取并打印线程局部变量的值
            SimpleDateFormat df = dateFormat.get();
            String formattedDate = df.format(new Date());
            System.out.println(Thread.currentThread().getName() + ": " + formattedDate);
            // 删除线程局部变量的值
            dateFormat.remove();
        };

        Runnable task2 = () -> {
            // 获取并打印线程局部变量的值
            SimpleDateFormat df = dateFormat.get();
            String formattedDate = df.format(new Date());
            System.out.println(Thread.currentThread().getName() + ": " + formattedDate);
            // 删除线程局部变量的值
            dateFormat.remove();
        };

        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        thread1.start();
        thread2.start();
    }
}

ThreadLocal是一种强大的工具,适用于需要线程隔离的场景,如用户会话、数据库连接和格式化对象等。使用ThreadLocal可以有效地管理线程本地的数据,避免多线程环境下的竞争和数据一致性问题。但是由于ThreadLocal的生命周期与线程相关,如果在线程池中使用ThreadLocal,需要注意及时调用remove()方法清理线程局部变量,来防止内存泄漏。

实现原理

ThreadLocal类本身并不存储线程本地变量的值,而是通过ThreadLocalMap来实现。每个线程内部都有一个ThreadLocalMap实例,ThreadLocal变量作为ThreadLocalMap的键,存储的值是该线程对应的变量值。

set方法首先获取当前线程 Thread 对象,然后获取该线程的 ThreadLocalMap 实例。如果存在,则将值存储在 ThreadLocalMap 中;否则,创建一个新的 ThreadLocalMap

java 复制代码
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 每个线程 都有一个自己的ThreadLocalMap
    // ThreadLocalMap 里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 向map里添加值
        map.set(this, value);
    else
        // map为null,创建一个 ThreadLocalMap
        createMap(t, value);
}

// 全局定义的localMap
ThreadLocal.ThreadLocalMap threadLocals = null;

// 获取当前线程所持有的localMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建,初始化 localMap 
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get方法同样先获取当前线程 Thread 对象,然后获取该线程的 ThreadLocalMap 实例。再通过 ThreadLocal 对象作为键从 ThreadLocalMap 中获取值。如果键不存在,则调用 setInitialValue 方法初始化变量。

java 复制代码
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 每个线程 都有一个自己的ThreadLocalMap,
    // ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}


// 全局定义的localMap
ThreadLocal.ThreadLocalMap threadLocals = null;

// 获取当前线程所持有的localMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

setInitialValue 方法通过 initialValue 方法获取初始值,并存储在 ThreadLocalMap 中。如果 initialValue 方法未被重写,默认返回 null

java 复制代码
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;
}

protected T initialValue() {
    return null;
}

ThreadLocalMap是一个自定义的哈希表,其中每个元素是一个Entry对象。ThreadLocalMap是一个比较特殊的Map,它的每个Entrykey都是一个弱引用。

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露。

内存泄漏问题

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收。但是Entry中的value依然是强引用,value的引用链条如下:

text 复制代码
Thread --> ThreadLocalMap --> Entry --> value

只有当Thread被回收时,这个value才有被回收的机会,否则只要线程不退出,value总是会存在一个强引用。但是要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话就会造成value对象出现泄漏的可能。

如果get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()remove(),那么这个内存泄漏依然会发生。所以当你不需要这个ThreadLocal变量时,主动调用remove(),这样是能够避免内存泄漏的。可以将ThreadLocal的使用和清理放在try-finally块中,确保remove()方法总是会被调用。

text 复制代码
ThreadLocal<MyClass> threadLocal = new ThreadLocal<>();

try {
    threadLocal.set(new MyClass());
    // 使用线程局部变量
} finally {
    threadLocal.remove();
}

除此之外,应尽量避免将ThreadLocal对象声明为静态变量,特别是在应用服务器或类似环境中,因为它们的生命周期通常较长,会增加内存泄漏的风险。

相关推荐
linweidong4 天前
唯品会Android面试题及参考答案
android·java多线程·内存泄漏·anr·aidl·安卓面试·安卓面经
pingzhuyan1 个月前
EasyExcel: 结合springboot实现表格导出入(单/多sheet), 全字段校验,批次等操作(全)
java·spring boot·servlet·threadlocal·easyexcel
cyt涛1 个月前
SpringCloudGateway — 网关登录校验
运维·网关·gateway·登录·过滤器·校验·threadlocal
艾伦~耶格尔2 个月前
【Java后端】之 ThreadLocal 详解
java·后端·学习·线程·threadlocal
cyt涛2 个月前
公共字段自动填充-MyBatis-Plus
java·数据库·mybatis·mybatis-plus·threadlocal·自动填充·公共字段
岁岁岁平安3 个月前
springboot实战学习(10)(ThreadLoacl优化获取用户详细信息接口)(重写拦截器afterCompletion()方法)
java·spring boot·后端·学习·threadlocal·jwt令牌
程序猿进阶3 个月前
ThreadLocal 释放的方式有哪些
java·开发语言·性能优化·架构·线程池·并发编程·threadlocal
栗筝i4 个月前
Java 并发编程:线程变量 ThreadLocal
threadlocal·栗筝i 的 java 技术栈·java 基础·java 并发·线程变量
少不入川。5 个月前
ThreadLocal源码分析
java·juc·threadlocal
_whitepure5 个月前
CAS详解
cas·java多线程·unsafe·aba问题·cas原理