【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类

这篇具有很好参考价值的文章主要介绍了【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言: 

ThreadLocal: 

ThreadLocal的内部结构: 

 ThreadLocal的常用方法:

1.set方法:

2.get方法:

3.setInitialValue方法

remove方法():

ThreadLocalMap:​编辑

成员变量:

存储结构:

 高频考点:

总结: 


前言: 

        当我们编写多线程程序时,经常会遇到一些需要在线程之间共享数据的情况。然而,共享数据可能会引发线程安全的问题,例如竞态条件(race condition)和数据覆盖等。为了解决这些问题,Java 提供了许多线程同步的机制,如 synchronized 关键字和 Lock 接口等。

然而,并不是所有的场景都适合使用传统的线程同步方式。有些情况下,我们更希望每个线程都拥有自己独立的数据副本,以避免线程之间的干扰。这时,ThreadLocal 就成为了一个非常有用的工具。

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

ThreadLocal: 

ThreadLocal 是 Java 提供的一个线程本地变量工具类,它为每个线程提供了一个独立的变量副本,使得每个线程都可以独立地操作自己的变量副本,而不会影响其他线程。这种变量在线程的生命周期内起作用。

总结:

        ThreadLocal可以在多线程环境下为线程创建变量副本,使得变量副本只存在于当前线程当中。而且是存在线程隔离的,每一个线程的变量副本都是相互隔离的,不会彼此影响。线程变量副本的存在缓解了线程在跨函数使用变量时的繁琐,并且可以为每一个线程单独记录数据

举例:假设我们有一个用户管理后端,而我们在查询的时候,需要记录当前使用者的查询次数。那么就会出现一个问题:如果我们只是简单的创建一个变量记录当前查询者的查询次数的话,那么所有的使用者的查询次数都会记录到这个变量当中,无法满足我们想要的记录当前使用者的查询次数,因为在代码中创建的这个变量是被所有线程所共享的。

那么在这个时候我们就需要使用ThreadLocal。创建一个为当前线程所独享的变量。通过这种方式,我们就使得每一个用户都有了自己的一个查询计数器。

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

最后我们来看一看源代码中的作者是如何描述ThreadLocal的作用的:

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

ThreadLocal的内部结构: 

早期设计:

        在JDK早期版本的时候,ThreadLocal的设计方案为:每一个ThreadLocal都创建一个Map,然后用线程作为MapKey,要存储的局部变量作为对应的value。这样就达到了各个线程的局部变量隔离的效果:

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

现在设计:

      每一个Thread维护一个ThredLocalMap,这个MapkeyThreadLocal本身,value才是真正要存储的值。

换句话来说:

       (1) 每一个Thread线程内部都有一个Map

       (2)  Map里面存储ThreadLocal对象(Key)线程的变量副本(Vlaue)

       (3) Thread内部的map是由ThreadLLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

 ThreadLocal的常用方法:

方法声明 描述
ThreadLocal() 创建ThreadLocal对象
set() 设置当前线程的变量副本
get() 获取当前线程的变量副本
remove() 移除当前线程的变量副本

1.set方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

set方法的整体逻辑为:先获取到当前线程,再根据当前线程获取到对应的ThreadLocalMap,如果map不为空就进行插入,如果map为空的话就创建一个map并且插入t和value。

2.get方法:

    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;
            }
        }
        return setInitialValue();
    }

get方法的整体逻辑为:先获取到当前线程,再根据当前线程获取到对应的ThreadLocalMap。如果map不为空的话,就根据当前的ThreadLocal为Key,调用getEntry获取到对应的存储实体e,如果e不为空的话,就获取到e所对应的value值。如果获取到的map为空的话,就执行初始化。

3.setInitialValue方法

    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);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

setInitialValue方法的整体逻辑为:获取到value值(这里使用的initialValue默认返回null),获取到当前线程。之后再根据当前线程查询是否有对应的map。如果当前线程有对应的map,那么就更新值。否则的话就进行创建。

最后instance of 关键字用来判断this的类型是否属于TerminatingThreadLocal。如果this属于是erminatingThreadLocal类型的,那么就调用register方法将this进行注册到TerminatingThreadLocal类中。

总结来说,最后的这段代码用于将终止类型的 ThreadLocal 实例注册到 TerminatingThreadLocal 类的静态列表中。这样,在线程退出时,终止类型的 ThreadLocal 实例会自动从 ThreadLocalMap 中移除,避免内存泄漏。

当一个线程终止时,JVM会自动调用Thread类中的ThreadLocal.ThreadLocalMap.threadTerminated()方法,该方法会遍历所有的ThreadLocal对象,并调用TerminatingThreadLocal类的terminate()方法。terminate()方法将删除该线程所有使用的TerminatingThreadLocal对象所对应的本地变量,从而释放内存。这种方式比较安全和可靠,因为即使用户没有手动清理线程本地变量,也不会造成内存泄漏。

remove方法():

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

 它调用了threadmap的remove方法,因此我们转到这个方法里面:

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

remove方法的整体逻辑为:根据ThreadLocal对象的哈希码,在ThreadLocalMap中定位并移除对应的Entry对象,以实现对ThreadLocal对象的移除操作。

其实通过这么多的源码分析我们能够看出:想要深入了解ThreadLocal,就一定要深入了解ThreadLocalMap。因此我们来介绍一下ThreadLocalMap 

ThreadLocalMap:

部分源码截取:

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

由此我们可以看出:ThreadLocalMap并没有实现Map接口。

需要注意的是:ThreadLocalMap虽然是ThreadLocal 的内部类,但是其实例却是由Thread对象维护的。

成员变量:

        /**
         * 初始容量 -- 必须是2的整次幂.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * 当前表中enrtys的个数.
         */
        private int size = 0;

        /**
         * 扩容阈值.
         */
        private int threshold; // Default to 0

在这里数组长度也必须是二的n次幂。道理和HashMap篇讲的一样:

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言

我们在使用remove方法的时候使用了哈希码和数组长度-1进行&运算。本来是哈希码%数组长度。但是为了提高效率我们采用位运算,如果想要哈希码和数组长度-1进行&运算的结果和哈希码%数组的结果一致,我们就需要让数组的长度等于二的n次幂。

更详细的解释可以看我HashMap的文章:

【从零开始学习JAVA集合 | 第一篇】深入解读HashMap源码(含面试题)-CSDN博客https://liyuanxin.blog.csdn.net/article/details/134867511

存储结构:

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

 高频考点:

1.ThreadLocal为什么会出现内存泄漏?

当Thread是线程池中的一个对象的时候就会发生内存泄漏。这是因为当ThreadLocal对象使用完之后,本来应该要对Entry对象进行回收。但是线程池中的线程不会被销毁,线程对象是通过强引用指向ThreadLocalMap的,ThreadLocalMap也是通过强引用指向Entry对象的,因此线程不被回收。Entry对象也不会被回收,这就出现了内存泄漏。

基于这种不稳定性,我们设置Entry中的Key(ThreadLocal)为弱引用,在下一次垃圾回收的时候回收掉这个Key。此时Key就为null了,而ThreadLocalMap的set/get/remove在检测到key==null的时候,会自动的把value清空,这样就实现了避免内存泄漏。

2.ThreadLocal的使用场景?

 1.Java中有的对象在被多个线程同时操作的时候就会报错,例如SimpleDateFormat对象。这个时候我们就可以使用ThreadLocal来为每一个线程都创建一个ThreadLocal对象。

2.全局存储用户信息,比如用户ID。我们就可以将其放到ThreadLocal对象当中,被线程独享。

其实我认为:ThreadLocal的作用不是去保护共享数据的安全性,而是去做数据的隔离。因此他和加锁关键字synchronized 并没有什么可比的。因为synchronized是做共享数据的安全性的。 

总结: 

        总的来说,ThreadLocal是Java中的一个线程局部变量,它为每个线程提供了一个独立的变量副本,保证了线程安全。ThreadLocal可以用于管理线程私有的数据,避免竞争条件,提高系统的并发性能。在实际应用中,ThreadLocal常用于数据库连接管理、用户登录信息管理、线程调试信息传递、消息传递等场景。

需要注意的是,虽然ThreadLocal可以有效地解决线程安全问题,但是过度使用ThreadLocal也可能会导致内存泄漏问题。因此,在使用ThreadLocal时,需要特别注意及时清理ThreadLocal中的数据,避免不必要的内存占用。

此外,还需要注意ThreadLocal的使用方式,尽量避免在多个线程之间共享ThreadLocal实例,以免出现意外的问题。同时,也应该考虑使用InheritableThreadLocal子类,以便在子线程中继承父线程的数据。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类,【从零开始学习JAVA重要集合】,java,学习,开发语言文章来源地址https://www.toymoban.com/news/detail-802248.html

到了这里,关于【从零开始学习Java重要知识 | 第二篇】深入解读ThreadLocal类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【从零开始学习JAVA | 第二十五篇】泛型

    目录 前言: 泛型: 额外拓展: 总结:         本文将详细介绍之前我们在JAVA 中一直在讲的泛型,各位感兴趣的同学可以点击进来观看。         泛型是一种编程概念, 它允许在定义类、接口或方法时使用类型参数,这样可以在使用时指定实际的类型。 通过使用泛

    2024年02月16日
    浏览(43)
  • 【从零开始学习JAVA | 第二十九篇】Stream流

    目录 前言: Stram流: 设计目标: 使用步骤: 1.先得到一条Stream流,并把数据放上去。 2.利用Stream流中的各种API进行操作。 使用Stream流的注意事项: 总结:         本文我们将学习Stream流,他就像流水线一样,可以对我们要处理的对象进行逐步处理,最终达到我们想要的效果

    2024年02月17日
    浏览(47)
  • 【从零开始学习JAVA | 第二十七篇】JAVA期末练习(PTA)

    目录 前言: R7-5 Count the letters in a string (统计字符串中的字符) R7-1 找素数 R7-3 电话号码同步(Java) 总结:         临近期末,我也更新一下PTA上的JAVA大题,希望各位都可以考出一个好的成绩。 (Count the letters in a string) (统计字符串中的字符) Write a method that counts th

    2024年02月16日
    浏览(65)
  • 【从零开始学习JAVA | 第二十三篇】集合体系结构

    目录 前言: 单列集合:      set与list的区别: 双列集合: map的特点: 总结:                   JAVA中为我们提供了很多集合,这些集合都有自己很独特的特点,因此我们要学习所有的集合,但是在学习所有的集合之前,我们还是先为大家介绍一下JAVA的集合体系结构,这

    2024年02月16日
    浏览(59)
  • 【从零开始学习JAVA | 第二十篇】常见API介绍 Math

    目录 前言:  MATH: Math类中的常用方法: 总结:                 本篇往后我们会详细介绍一些常用的API,今天我们介绍的是Math的常用方法。但是其实我们不需要记住所有的方法,在日常工作中自己学会查询API文档就可以了。 Math类是Java中提供的一个标准类, 它包含了许多

    2024年02月15日
    浏览(55)
  • 【从零开始学习JAVA | 第二十一篇】常见API介绍 System

    目录 前言: System: System类中静态方法: 总结:                 system 是一个很底层的 API,是一个工具类, 提供了一些与系统相关的方法 。他在我们写项目的时候提供了一些非常实用的方法,本篇将会介绍一些实用的system API 的方法调用。 System类是Java语言提供的一个

    2024年02月15日
    浏览(62)
  • 【从零开始学习JAVA | 三十九篇】深入多线程

    目录 前言:         ​1.线程的寿命周期​ 2.线程的安全问题 3.锁 同步代码块: 同步方法: 死锁: 4.生产者和消费者模式(等待唤醒机制) 总结:         当今软件开发领域中,多线程编程已成为一项至关重要的技能。然而,要编写出高效、可靠的多线程程序并不容

    2024年02月13日
    浏览(62)
  • 深入浅出Java的多线程编程——第二篇

    目录 前情回顾 1. 中断一个线程 1.1 中断的API 1.2 小结 2. 等待一个线程  2.1 等待的API 3. 线程的状态 3.1 贯彻线程的所有状态 3.2 线程状态和状态转移的意义 4. 多线程带来的的风险-线程安全 (重点) 4.1 观察线程不安全 4.2 线程安全的概念 4.3 线程不安全的原因 4.3.1 修改共享数据

    2024年02月07日
    浏览(81)
  • 【从零开始学习JAVA | 第四十一篇】深入JAVA锁机制

    目录 前言:          引入: 锁机制:  CAS算法: 乐观锁与悲观锁: 总结: 在多线程编程中,线程之间的协作和资源共享是一个重要的话题。当多个线程同时操作共享数据时,就可能引发数据不一致或竞态条件等问题。为了解决这些问题,Java提供了强大的锁机制,使得

    2024年02月14日
    浏览(54)
  • Java学习手册——第二篇面向对象程序设计

    第一章我们已经介绍了Java语言的基础知识,也知道他能干什么了, 那我们就从他的设计思想开始入手吧。 接触一个语言之前首先要知道他的大方向,设计思想是什么样的, 这样接触起来就知道基本原理,才能事半功倍。 迪克斯特拉(E.W.dijkstra)在 1965 年 提出结构化编程,

    2024年02月12日
    浏览(63)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包