让我们谈谈你对 ThreadLocal 的理解

这篇具有很好参考价值的文章主要介绍了让我们谈谈你对 ThreadLocal 的理解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

介绍 ThreadLocal

从 JDK1.2 开始,ThreadLocal 是一个被用来存储线程本地变量的类。在 ThreadLocal 中的变量在线程之间是独立的。当多个线程访问 ThreadLocal 中的变量,它们事实上访问的是自己当前线程在内存中的变量,这能确保这些变量是线程安全的。

我们通常使用 ThreadLocal 解决线程中的变量冲突问题。事实上,解决这类问题,我们通常考虑使用 synchronized。

例如,当解决 SimpleDateFormat 的线程安全问题时,SimpleDateFormat 不是线程安全的。不管 format() 方法或 parse() 方法,它使用了自己的内部 Calendar 对象。format() 方法设置时间,parse() 方法调用 Calendar.first.clear() 方法,再调用 Calendar 类的 set() 方法。如果一个线程刚刚调用 set(),期间有另一个线程直接调用了 clear() 方法,会导致 parse() 方法出现问题。

第一种解决办法,对使用了 SimpleDateformat 的方法加 synchronized,尽管线程安全有了保障,但是效率(吞吐量)变低了。一段时间内只有一个线程能使用这个方法。

 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 public static synchronized String formatDate(Date date){
     return simpleDateFormat.format(date);
 }

第二种解决办法,将 SimpleDateFormat 对象放入 ThreadLocal,所以,每一个线程都有了自己的 SimpleDateFormat 对象。它们不会互相干扰,保证了线程安全。

 private static final ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(
     () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
 );public static String formatDate(Date date)
 {
    return simpleDateFormatThreadLocal.get().format(date);
 }

ThreadLocal 的基本用法

下面看看如何使用 ThreadLocal:

 ThreadLocal<Integer> threadLocal99 = new ThreadLocal<Integer>();
 threadLocal99.set(3);
 int num = threadLocal99.get();
 System.out.println("Number:"+num);
 threadLocal99.remove();
 System.out.println("Digital Empty:"+threadLocal99.get());

运行结果:

 Number: 3
 Number Empty: null

ThreadLocal 非常容易使用。main 线程向 ThreadLocal 中放入了多个变量,它们在线程执行期间都能访问。当线程执行结束,变量都将被销毁。只要没有调用 remove() 方法,当前线程都能从 ThreadLocal 获取到数据。

因为 ThreadLocal 存放在当前执行线程中,ThreadLocal 中的变量值仅能在当前线程(当前线程的子线程也可以获取到)中使用,这保证了线程安全。

让我们看一下 ThreadLocal 类的 set() 方法的源码:

 public void set(T value) {
     // Get the current thread
     Thread t = Thread.currentThread();
     // Get ThreadLocalMap
     ThreadLocal.ThreadLocalMap map = getMap(t);
     // Whether the ThreadLocalMap object is empty, if it is not empty, put the data directly into the ThreadLocalMap
     if (map != null)
         map.set(this, value);
     else
         createMap(t, value); // If the ThreadLocalMap object is empty, create the object first, and then assign the value.
 }

我们看到变量都存储在 ThreadLocalMap 变量中。所以,ThreadLocalMap 从哪里来的?

 ThreadLocalMap getMap(Thread t) {
     return t.threadLocals;
 }
 public class Thread implements Runnable {
     ......
     /* ThreadLocal values pertaining to this thread.This map is maintained
      * by the ThreadLocal class.*/
     ThreadLocal.ThreadLocalMap threadLocals = null;
     ......
 }

通过上面的源码,我们可以发现 ThreadLocalMap 变量是当前执行线程的一个变量,所以,存储在ThreadLocal 中的数据其实都是存放在当前线程的 threadLocals 变量中。threadLocals 变量存放在当前线程对象中。对于另一个线程,就是另一个线程对象。另一个线程对象中的数据是不能获取到的,所以 ThreadLocal 是隔离的。

ThreadLocalMap 如何存储数据呢?

ThreadLocalMap 是 ThreadLocal 的内部类。尽管名字中包含 Map,它并没有实现 Map 接口,但是结构和 Map 类似。

让我们谈谈你对 ThreadLocal 的理解

ThreadLocalMap 事实上是一个由 Entry 组成的 array,Entry 是 ThreadLocalMap 的一个内部类,继承了 WeakReference,同时将 Entry 的 key 设置成了 ThreadLocal 对象,并且还将 key 设置成了弱引用。ThreadLocalMap 的内部结构大概就是一个 Entry 的集合,其中 Entry 中的 key 和 value 的类型是固定的。

让我们谈谈你对 ThreadLocal 的理解

它和真正的 Map 还是有一些区别的,没有链表,这就意味着解决 hash 冲突的方法和 HashMap 不同(HashMap 采用的是拉链法,ThreadLocalMap 采用的是线性探测)。

一个线程中可以创建多个 ThreadLocal 对象,多个 ThreadLocal 对象可以存储多个数据,这些数据将会存储在 ThreadLocalMap 的 array 中。

接下来看看 ThreadLocalMap 中比较特别的方法 set():

 /**
  * Set the value associated with key.
  * @param key the thread local object
  * @param value the value to be set
  */
 private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at
     // least as common to use set() to create new entries as
     // it is to replace existing ones, in which case, a fast
     // path would fail more often than not.Entry[] tab = table;
     int len = tab.length;
     // Position in the array
     int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];
          e != null;
          e = tab[i = nextIndex(i, len)]) {
         ThreadLocal<?> k = e.get();
         // If the current position is not empty, and the key at the current position is equal to the passed key, then the data at the current position will be overwritten
         if (k == key) {
             e.value = value;
             return;
         }
         // If the current position is empty, initialize an Entry object and place it at the current position.
         if (k == null) {
             replaceStaleEntry(key, value, i);
             return;
         }
     }
     // If the current position is not empty, and the key at the current position is not equal to the key to be assigned, then the next empty position will be found and the data will be placed directly in the next empty position.
     tab[i] = new Entry(key, value);
     int sz = ++size;
     if (!cleanSomeSlots(i, sz) && sz >= threshold)
         rehash();
 }

我们可以从 set() 方法中看出,在代码逻辑层面分为了 4 步:

第一步,通过对 ThreadLocal 对象的 hash 值和 array 的数组长度求 AND,得到数据放入当前 array 的那个位置

第二步,判断当前位置是否为空,如果为空,直接初始化一个 Entry 对象并将其放入这个位置

第三步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 相等,将当前位置的数据覆盖掉

第四步,如果当前位置不为空,并且当前位置的 Entry 的 key 和传入的 key 不相等,于是向后查找下一个不为空的位置,将数据放入空位置中(当遍历到 array 的末尾,会执行扩容操作)

get() 方法采用的是相同的逻辑。首先通过传入的 ThreadLocal 的 hash 值获取到元素在 Entry Array 中的位置,然后比较当前位置的 Entry 的 key 是否和 传入的 ThreadLocal 对象相同,如果相同,直接返回当前位置的数据,如果不相同,就需要判断 key 是否和 array 中的下一个元素相同。

 private Entry getEntry(ThreadLocal<?> key) {
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     if (e != null && e.get() == key)
         return e;
     else
         return getEntryAfterMiss(key, i, e);
 }
 /**
  * Version of getEntry method for use when key is not found in
  * its direct hash slot.
  *
  * @param  key the thread local object
  * @param  i the table index for key's hash code
  * @param  e the entry at table[i]
  * @return the entry associated with key, or null if no such
  */
 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
     Entry[] tab = table;
     int len = tab.length;
     while (e != null) {
         ThreadLocal<?> k = e.get();
         if (k == key)
             return e;
         if (k == null)
             expungeStaleEntry(i);
         else
             i = nextIndex(i, len);
         e = tab[i];
     }
     return null;
 }

我们已经说过 ThreadLocal 是存放在单个线程中的,每一个线程都会有自己的数据,但是 ThreadLocal 中真正的对象数据是存放在堆中的,线程对象仅仅保存了对象的引用。

当我们使用 ThreadLocal 的时候,我们通常需要在执行的方法的上下文中共享线程局部的变量。例如,我的 main 线程执行方法中的代码,但是,方法中有一段代码创建了一个新的线程,这个方法中定义的 ThreadLocal 中的变量也被这个新创建的线程使用了…这种情况下,有必要从新线程中调用外部线程的数据,这些数据需要在线程之间共享。ThreadLocal 支持父子线程共享数据这种情况。比如:

 ThreadLocal threadLocalMain = new InheritableThreadLocal();
 threadLocalMain.set("Main thread variable");
 Thread t = new Thread() {
     @Override
     public void run() {
         super.run();
         System.out.println( "The variable obtained now is = "+ threadLocalMain.get());
     }
 };
 t.start();

执行结果:

 The variable obtained now is = main thread variable

上面的代码可以实现在父线程和子线程中共享数据,关键是通过 InheritableThreadLocal 去实现共享数据。

所以,它是如何做到的呢?

下面是在 Thread 类的 init() 方法中的一小段代码:

 if (inheritThreadLocals && parent.inheritableThreadLocals != null)
     this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码意味着,当创建一个线程时,如果当前线程的 inheritThreadLocals 变量存在,并且,父线程中的 inheritThreadLocals 变量不为 null,父线程的 inheritThreadLocals 变量中的数据会赋值给当前线程中的 inheritThreadLocals 变量。

ThreadLocal 的内存泄漏问题

我们前面介绍过,Entry 类是继承了 WeakReference 类,表明了 Entry 的 key 是弱引用。

让我们谈谈你对 ThreadLocal 的理解

弱引用类型用于描述非必须的对象。弱引用类型对象仅能存活到下一次垃圾收集之前。当垃圾收集器开始工作,不管当前内存是否充足,都会回收那些只有弱引用的对象。

弱引用依然是 ThreadLocal 对象本身,所以,通常来所,当线程执行完毕,ThreadLocal 对象会变成 null,并且弱引用对象如果为 null 就会在下一次 GC 时被清理,所以,Entry key 所占用的空间会被释放,但是 Entry 的 value 依然会占用内存,如果当前线程被重新使用(例如线程在线程池中),并且之后 ThreadLocal 不再被使用(用来从 ThreadLocalMap 获取数据),所以,ThreadLocalMap 中对应的 Entry 中的 value 会一直存在,这将会导致内存泄漏。 为了避免内存泄漏,在 ThreadLocal 使用后需要立即执行 remove() 方法去释放 key 和 value 占用的内存。

内存泄漏还是会发生,那为什么 ThreadLocal 需要设置为弱引用?

在一般情况下,对象是强引用的,但是,只要引用一直存在,强引用对象就不会被回收,所以,如果 Thread 被重用,在 Entry 中的 Key 和 Value 都不会被回收,这回导致 Key 和 value 都会占用内存。

但是,如果将 ThreadLocal 设置为弱引用,当 ThreadLocal 没有被强制引用时,它可以被回收。回收后,Entry 中的 key 变成 null。如果线程被重复使用,只要 ThreadLocal 被用来从 ThreadLocalMap 中获取数据,ThreadLocal 的 set、get 和其他方法就会被调用。当 set、get 和其他方法被调用,那些 Entry 中 key 是 null 的数据可能(get 方法:如果目标 Entry 的 key 为 null(回收了)或着发生了 Hash 冲突时,set 方法:启发式扫描。看源码)被扫描到。当发现 Entry 中有 Entry 的 key 是 null,这个 Entry 的 value 也会被设置为 null,所以,这个 Entry 的 value 也会被回收,这样可以进一步的避免内存溢出,当 ThreadLocalMap 重新 hash 的时候,也可以首先将那些 key 是 null 的 Entry 清理掉,如果 ThreadLocalMap 的空间不足,在执行扩容操作。

尽管 Entry 的 key 设置为弱引用,如果线程被重用并且 ThreadLocal 在后来的任务执行过程中不被使用,Entry 中的 value 还是占用内存,最终还是会导致内存溢出,所以,当使用 ThreadLocal 的时候,最终一定要调用 remove() 方法。文章来源地址https://www.toymoban.com/news/detail-415643.html

到了这里,关于让我们谈谈你对 ThreadLocal 的理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 1、什么是面向对象?谈谈你对面向对象的理解

    对比面向过程,是两种不同的处理问题的角度 面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么 比如 : 洗衣机洗衣服 面向过程会将任务拆解成一系列的步骤(函数),1、打开洗衣机 …2、放衣服…3、放洗衣粉…4、清洗…

    2024年02月13日
    浏览(9)
  • 【面试八股文】每日一题:谈谈你对异常的理解

    【面试八股文】每日一题:谈谈你对异常的理解

    每日一题-Java核心-谈谈你对异常的理解【面试八股文】   异常是程序在运行过程中出现的错误或不正常的情况。当程序执行过程中遇到无法处理的错误或者不符合预期的情况,就会抛出异常。异常可以分为两种类型: 受检异常 和 非受检异常 。    受检异常 是指在程序

    2024年02月13日
    浏览(14)
  • 【面试八股文】每日一题:谈谈你对线程的理解

    【面试八股文】每日一题:谈谈你对线程的理解

    每日一题-Java核心-谈谈你对线程的理解【面试八股文】   Java线程是Java程序中的执行单元。一个Java程序可以同时运行多个线程,每个线程可以独立执行不同的任务。线程的执行是并发的,即多个线程可以同时执行。   Java中的线程有如下的特点 轻量级:线程的创建和销毁

    2024年02月12日
    浏览(9)
  • Python常见面试题016. 请实现如下功能|谈谈你对闭包的理解

    摘自流畅的python 第七章 函数装饰器和闭包 实现一个函数(可以不是函数)avg,计算不断增加的系列值的平均值,效果如下 跟Python常见面试题015.请实现一个如下功能的函数有点类似,但又不太一样 关键是你需要有个变量来存储历史值 参考代码 avg是个Average的实例 avg有个属性

    2023年04月10日
    浏览(8)
  • 分布式 - 谈谈你对分布式的理解,为什么引入分布式?

    分布式 - 谈谈你对分布式的理解,为什么引入分布式?

    不啰嗦,我们直接开始! 真正了解分布式系统的概念,日后工作中具有分布式系统设计思想。 能否在设计中对系统稳定性方面考虑周全。 能构建高 QPS 健壮的系统架构。 问题分析: 各种分布式框架层出不穷,Spring Cloud,阿里的 Dubbo,无论使用哪一个,原理都相同,考察下基

    2024年02月15日
    浏览(14)
  • 阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。

    ThreadLocal 在Java多线程编程中扮演着重要的角色,它提供了一种线程局部存储机制,允许每个线程拥有独立的变量副本,从而有效地避免了线程间的数据共享冲突。ThreadLocal的主要用途在于,当需要为每个线程维护一个独立的上下文变量时,比如每个线程的事务ID、用户登录信

    2024年03月24日
    浏览(9)
  • 我们来谈谈websocket

    我们来谈谈websocket

     \\\"你一无所有地闯荡。\\\"          WebSocket 是一种在单个TCP连接上进行全双工通信的协议。          WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可

    2024年02月12日
    浏览(8)
  • 我们来谈谈https

    我们来谈谈https

              \\\"这一封信只是得到它要回答问题,那个答案早已点燃在心里\\\"          紧接上文这仍然是一款拙劣的http服务器,我们此时在用户数输入栏输入数据信息并提交表单。我们先来认识认识使用到的两个工具软件。  postman是一款支持http协议的接口调试与测试工具

    2024年02月09日
    浏览(6)
  • 我们来谈谈tcp

    我们来谈谈tcp

    \\\"让新离开地表,才能找到盘旋爬升的动力。\\\"          我们先来认识认识tcp协议报头字段。          跟tcp协议字段报头比起来,udp可真是太轻松了。 协议字段 作用 源/目的端口号 从哪里来,到哪里去 32位序号/32位确认号 就是一种序号(之后会细讲) 4位首部字段 表示TCP头部

    2024年02月12日
    浏览(6)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包