您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

一文看懂Java中的ThreadLocal源码和注意事项

时间:2023-04-12 15:17:55  来源:今日头条  作者:Code404

一、ThreadLocal的原理

ThreadLocal是一个非常重要的类,它为每个线程提供了一个独立的变量副本。因此,每个线程都可以独立地访问和修改该变量,而不会影响其他线程的访问。这种机制在多线程编程中非常有用,特别是在需要处理线程安全问题时。

ThreadLocal的原理很简单:它为每个线程维护一个Map,该Map中存储了每个线程对应的变量值。当我们调用ThreadLocal的get()方法时,它将先获取当前线程,然后从当前线程的Map中查找对应的变量;如果该变量不存在,那么就通过initialValue()方法来创建一个新的变量,并将其存储到当前线程的Map中。在以后的访问中,该线程便可以直接从Map中获取变量值,而不需要通过参数传递或共享变量的方式。

二、源码关键片段讲解

1.get方法

用于获取当前线程的 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.currentThread() 方法获取当前线程对象 t
        Thread t = Thread.currentThread();
        // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 map
        ThreadLocalMap map = getMap(t);
        // 如果当前线程已经存储了该 ThreadLocal 对象对应的值,那么就通过 map.getEntry(this) 方法得到该值的 Entry 对象 e,并返回其 value 属性即可
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 否则,需要进行初始化,并将初始化后的值保存到当前线程中。这里调用的是 setInitialValue() 方法,其实现在类的另一个方法中。
        return setInitialValue();
    }

2.setInitialValue方法

用于初始化当前线程的 ThreadLocalMap 对象并将初始化后的值保存到其中。

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        // 调用 initialValue() 方法获取 ThreadLocal 对象的初始值
        T value = initialValue();
        // 通过 Thread.currentThread() 方法获取当前线程对象 t
        Thread t = Thread.currentThread();
        // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 如果 map 不为空,就使用 map.set(this, value) 方法
            // 将 ThreadLocal 对象和其对应的值存储到 map 中,
            // 即将该对象和值作为 key-value 存入 map 的 table 数组的某个位置,
            // table 的索引通过 hashCode() 方法进行计算并取模得到;
            map.set(this, value);
        } else {
            // 否则,就需要创建新的 ThreadLocalMap 对象,
            // 然后再将其存储到当前线程的 
            // ThreadLocalMap.ThreadLocalMapHolder.threadLocalMap 中。
            // 这里调用的是 createMap(t, value) 方法,其实现在类的另一个方法中。
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            // 如果该 ThreadLocal 对象是 TerminatingThreadLocal 的实例,
            // 那么就需要调用 TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this) 方法进行注册
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

3.createMap方法

用于为当前线程创建 ThreadLocalMap 对象,并将该对象存储到当前线程的 threadLocals 属性中

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    // 接受两个参数:当前线程 t 和 ThreadLocal 对象的初始值 firstValue 
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

将新创建的 ThreadLocalMap 对象作为参数传入 ThreadLocalMap 的构造方法,并将得到的结果存储到当前线程的 threadLocals 属性中。需要注意的是,线程的 threadLocals 属性是一个
ThreadLocal.ThreadLocalMap 类型的变量,表示线程的本地变量表。如果当前线程没有 threadLocals 属性,则会新建一个。如果当前线程已经有了 threadLocals 属性,则直接使用新建的 ThreadLocalMap 替换原来的对象即可。

需要强调的是,这里的 createMap() 方法是 ThreadLocal 类的一个 protected 方法,因此只能在 ThreadLocal 类及其子类中被调用。同时,在 InheritableThreadLocal 类中还有一个覆盖了该方法的版本,用于处理可以被子线程继承的线程本地变量。

4.remove方法

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplAIn #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         // 通过 Thread.currentThread() 方法获取当前线程对象 t
         // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 m
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             // 如果 m 不为 null,就调用 m.remove(this) 方法将该 ThreadLocal 从 m 中删除
             m.remove(this);
         }
     }

注意,remove() 方法只会删除当前线程的 ThreadLocalMap 中与该 ThreadLocal 对象对应的 entry,不会影响其他线程的值。

三、ThreadLocalMap的Entry为什么是弱引用?

ThreadLocal的内部类ThreadLocalMap源码可以看到,它的Entry继承WeakReference,是一个弱引用的类

   /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

其实使用弱引用是为了避免内存泄漏问题。

ThreadLocalMap 中的 key 为 ThreadLocal 对象,value 为线程本地变量对应的副本。当一个线程结束时,如果不显式地清理 ThreadLocalMap 中该线程对应的 Entry 对象,那么这些 Entry 对象及其对应的 value 副本会一直存在于内存中,就会导致内存泄漏问题。

如果 ThreadLocalMap 中使用的是强引用,那么即使线程已经结束,由于这些 Entry 对象仍然被 ThreadLocalMap 引用着,垃圾回收器也无法将这些 Entry 对象回收掉,从而导致内存泄漏。

因此,为了避免这种情况,ThreadLocalMap 中使用了 WeakReference 来引用 ThreadLocal 对象。WeakReference 对象具有弱引用特性,当 ThreadLocal 变量没有被其他对象引用时,就可以被垃圾回收器回收,同时 ThreadLocalMap 中对应的 Entry 对象也会被自动删除,从而避免了内存泄漏问题。

特别需要注意的是,虽然 ThreadLocalMap 中的 Entry 对象是弱引用,但是 value 部分却是强引用,因此当 ThreadLocal 变量被垃圾回收器回收后,对应的 value 副本仍然会一直存在于内存中,需要开发者手动清理。因此,在使用 ThreadLocal 时,需要注意在不需要使用 ThreadLocal 变量时,及时调用 remove() 方法进行清理。

四、代码示例和解释

下面是一个简单的ThreadLocal使用示例:

public class ThreadLocalTest {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int num = threadLocal.get();
                    System.out.println(Thread.currentThread().getName() + " get num=" + num);
                    threadLocal.set(num + 1);
                    System.out.println(Thread.currentThread().getName() + " set num=" + (num + 1));
                    System.out.println(Thread.currentThread().getName() + " get num=" + threadLocal.get());
                }
            }).start();
        }
    }
}

上面的代码中,我们使用了一个静态变量threadLocal,它是一个ThreadLocal对象。我们在main方法中创建了5个线程,每个线程中都调用了threadLocal的get()方法来获取变量值,并输出线程名和变量值。然后,调用了threadLocal的set()方法来修改变量值,并输出线程名和修改后的变量值。最后再次调用get()方法来获取变量值,并输出线程名和变量值。

Thread-1 get num=0
Thread-1 set num=1
Thread-1 get num=1
Thread-4 get num=0
Thread-4 set num=1
Thread-4 get num=1
Thread-3 get num=0
Thread-3 set num=1
Thread-3 get num=1
Thread-2 get num=0
Thread-2 set num=1
Thread-2 get num=1
Thread-0 get num=0
Thread-0 set num=1
Thread-0 get num=1

我们可以看到,虽然这个变量是static的,但是每个线程都有自己的变量副本,它们互不干扰。这就避免了线程安全问题的出现。

五、注意事项

在使用ThreadLocal时,需要注意以下几点:

1.内存泄漏问题:ThreadLocal 中的变量可以在程序执行结束后一直存在于内存中,如果不及时清理,会导致内存泄漏问题。因此,在使用完 ThreadLocal 变量之后,需要手动调用 remove() 方法,将变量和当前线程绑定的副本从 ThreadLocalMap 中移除,并释放相关内存空间。

2.资源占用问题:ThreadLocal 对象本身也是一个对象,使用过多的 ThreadLocal 对象会占用大量的内存空间。因此,应该根据实际情况,合理地使用和管理 ThreadLocal 对象,避免过度使用。

3.只适用于线程内部传递数据:ThreadLocal 对象只能在同一个线程内部传递数据,无法在多个线程之间直接传递数据。



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: Java  点击:(5)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践&mdash;&mdash;如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: Java  点击:(27)  评论:(0)  加入收藏
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(10)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: Java  点击:(20)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20  Search: Java  点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18  Search: Java  点击:(25)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: Java  点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: Java  点击:(2)  评论:(0)  加入收藏
面向AI工程的五大JavaScript工具
令许多人惊讶的是,一向在Web开发领域中大放异彩的JavaScript在开发使用大语言模型(LLM)的应用程序方面同样大有价值。我们在本文中将介绍面向AI工程的五大工具,并为希望将LLM...【详细内容】
2024-02-06  Search: Java  点击:(53)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(14)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(25)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(57)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(68)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(72)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(89)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(106)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(97)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(75)  评论:(0)  加入收藏
站内最新
站内热门
站内头条