• ThreadLocal详解


    ThreadLocal

    ThreadLocal文档注释

    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。API方法也相应进行了调整,新版本的API方法分别是void set(T value)T get()

    ThreadLocal中主要有三个方法:

    • set():设置当前线程的线程局部变量的值。
    • get():该方法返回当前线程所对应的线程局部变量。
    • remove():删除当前线程的线程局部变量,目的是为了减少内存的占用。
    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的值。

    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

    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 方法初始化变量。

    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

    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都是一个弱引用。

    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的引用链条如下:

    Thread --> ThreadLocalMap --> Entry --> value
    

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

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

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

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

  • 相关阅读:
    python: float64与float32转换、压缩比较与转换偏差
    通信原理学习笔记3-4:数字通信系统性能指标(带宽、信噪比Eb/N0、可靠性与误码率、有效性与频谱利用率)
    04 YAML kubetnetes世界里的通用语
    【数据结构】队列和栈
    HJ20 密码验证合格程序
    这五个适合上班族的副业你知道多少
    用浏览器快速开启Docker的体验之旅
    工具箱之 IKVM.NET 项目新进展
    paddledetection在window使用cpu快速上手 & 在cpu端训练自己的VOC类型数据集
    使用 Spacesniffer 找回 48G 系统存储空间的总结
  • 原文地址:https://blog.csdn.net/white_pure/article/details/140407691