Java 中的线程局部变量ThreadLocal和InheritableThreadLocal分析

本文主要是介绍Java 中的线程局部变量ThreadLocal和InheritableThreadLocal分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

1、什么是ThreadLocal变量?

2、ThreadLocal 的实现原理

3、为什么需要ThreadLocal变量?

4、ThreadLocal 的使用示例

5、ThreadLocal 的内存泄露问题

6、什么是InheritableThreadLocal变量?


1、什么是ThreadLocal变量?

        ThreadLocal变量是Java中的一种线程局部变量,它为每个线程提供了独立的变量副本。在多线程环境下,每个线程可以独立地访问和修改自己的ThreadLocal变量副本,而不会对其他线程产生影响// Thread类中的一个属性

        每个ThreadLocal对象都维护着一个独立的变量副本,这些副本存储在ThreadLocal对象的内部数据结构中,通常是一个Map。当线程访问ThreadLocal变量时,实际上是通过当前线程获取到自己的变量副本。

        在使用ThreadLocal变量时,首先需要创建一个ThreadLocal对象,并通过set()方法将变量的值设置到当前线程的变量副本中。然后,可以通过get()方法获取当前线程的变量副本的值。每个线程通过自己的ThreadLocal对象访问变量,从而实现了线程间的隔离

2、ThreadLocal 的实现原理

        首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。

        因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 set 方法:// 获取当前线程的ThreadLocalMap

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

        get方法:

public T get() {   Thread t = Thread.currentThread();   ThreadLocalMap map = getMap(t);   if (map != null)   return (T)map.get(this);   // Maps are constructed lazily.  if the map for this thread   // doesn't exist, create it, with this ThreadLocal and its   // initial value as its only entry.   T value = initialValue();   createMap(t, value);   return value;   }

        createMap方法:

    void createMap(Thread t, T firstValue) {   t.threadLocals = new ThreadLocalMap(this, firstValue);   } 

        ThreadLocalMap是个静态的内部类:

    static class ThreadLocalMap {   ........   } 

        最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

3、为什么需要ThreadLocal变量?

        ThreadLocal变量在多线程编程中有以下几个重要的用途和好处:

  1. 线程隔离:每个线程都有自己独立的ThreadLocal变量副本,不同线程之间互不干扰。这种隔离性使得每个线程可以独立地访问和修改自己的变量副本,而无需担心其他线程的影响。这对于需要在线程间保持状态或上下文的场景非常有用,例如在Web应用程序中存储用户的会话信息。
  2. 线程上下文传递:ThreadLocal变量可以在线程间传递上下文信息,而无需显式地传递或复制变量。例如,当一个线程创建子线程时,子线程可以继承父线程的ThreadLocal变量副本,从而获得父线程的上下文信息。这在一些需要父子线程之间共享状态的场景中非常有用。
  3. 线程安全性:由于每个线程都有自己的变量副本,ThreadLocal变量可以减少对共享数据的并发访问,从而简化了线程安全性的问题。每个线程对自己的ThreadLocal变量进行操作,不需要进行额外的同步措施。这有助于降低多线程编程的复杂性和线程竞争的风险。
  4. 性能优化:ThreadLocal变量可以提高多线程程序的性能。在某些场景下,如果多个线程频繁地读写共享变量,可能会引入线程竞争和同步开销。通过使用ThreadLocal变量,可以避免对共享变量的频繁读写操作,从而减少了线程间的竞争和同步开销,提升了程序的性能

4、ThreadLocal 的使用示例

        下面是一个简单的示例,演示了如何在多线程环境中使用ThreadLocal变量:

public class ThreadLocalExample {private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {// 创建并启动两个线程Thread thread1 = new Thread(() -> {threadLocal.set(1);System.out.println("Thread 1 - ThreadLocal value: " + threadLocal.get());// 可以在此处进行其他操作threadLocal.remove(); // 清理ThreadLocal变量});Thread thread2 = new Thread(() -> {threadLocal.set(2);System.out.println("Thread 2 - ThreadLocal value: " + threadLocal.get());// 可以在此处进行其他操作threadLocal.remove(); // 清理ThreadLocal变量});thread1.start();thread2.start();thread1.join();thread2.join();}
}

        在上面的示例中,我们创建了一个静态的ThreadLocal变量threadLocal,用于存储整数值。在thread1线程中,我们将变量值设置为1,并打印输出。在thread2线程中,我们将变量值设置为2,并打印输出。最后,我们通过调用remove()方法来清理ThreadLocal变量,以确保在线程结束后不会产生内存泄漏

        值得注意的是,每个线程通过get()和set()方法访问和设置自己的ThreadLocal变量副本,而无需担心其他线程的影响。这样,每个线程都能独立地操作自己的ThreadLocal变量,实现了线程间的隔离。

5、ThreadLocal 的内存泄露问题

        ThreadLocal在使用不当的情况下可能导致内存泄漏问题。这种内存泄漏通常发生在长时间运行的应用程序中,尤其是在使用线程池或者使用ThreadLocal的框架或库中。

        ThreadLocal内存泄漏问题的原因是,ThreadLocalMap中的Entry对象持有ThreadLocal对象的弱引用作为键,而值则是线程的变量副本。如果在ThreadLocal没有及时清理的情况下,ThreadLocal对象没有被及时垃圾回收,就会导致Entry对象无法被回收,从而引发内存泄漏。

        主要有两种情况可能导致ThreadLocal内存泄漏:// 总结起来就是线程长时间存活

  1. 线程长时间存活:如果一个线程长时间存活,而ThreadLocal对象没有被及时清理或移除,那么ThreadLocalMap中的Entry对象会一直存在,并持有ThreadLocal对象的弱引用。这样,ThreadLocal对象无法被垃圾回收,就会导致内存泄漏。
  2. 使用线程池:在使用线程池的情况下,ThreadLocal对象的生命周期可能超出单个任务的执行周期。当一个线程从线程池中取出并执行任务时,ThreadLocal对象会被创建并与该线程关联。但是,线程执行完任务后,并不会立即销毁,而是将线程放回线程池中等待下次任务。如果ThreadLocal对象没有被正确清理或移除,就会造成内存泄漏。

为避免ThreadLocal内存泄漏问题,可以采取以下措施:

  1. 及时清理:在不再需要使用ThreadLocal变量时,应调用remove()方法从ThreadLocalMap中清除对应的键值对。可以通过在finally块中调用remove()方法来确保在线程执行结束时清理ThreadLocal变量。
  2. 使用弱引用:在某些情况下,可以使用弱引用来持有ThreadLocal对象,从而让ThreadLocal对象能够在没有强引用时被垃圾回收。这可以通过继承ThreadLocal并重写initialValue()方法,在返回变量副本之前,将ThreadLocal对象包装为弱引用类型来实现。
  3. 防止线程长时间存活:在一些场景中,可以通过一些手段,如定时任务或线程终止时的清理机制,来确保线程在不再需要时能够及时终止。

        因此,ThreadLocal内存泄漏问题可能发生在长时间运行的应用程序或使用线程池的情况下。为避免内存泄漏,需要及时清理不再需要的ThreadLocal变量,并注意线程的生命周期,以确保ThreadLocal对象能够被垃圾回收。

6、什么是InheritableThreadLocal变量?

        InheritableThreadLocal(可继承的线程局部变量)是ThreadLocal的一个子类,它提供了一种特殊的ThreadLocal变量,允许子线程继承父线程中设置的值。

        在默认情况下,ThreadLocal变量的值在子线程中无法继承。但是,当使用InheritableThreadLocal时,可以通过父线程设置的值在子线程中进行传递和共享。

        InheritableThreadLocal的特点如下:

  1. 继承父线程值:当在父线程中设置InheritableThreadLocal的值后,子线程将会自动继承该值,而不需要显式地将值传递给子线程。
  2. 适用于线程继承场景:InheritableThreadLocal通常在需要子线程访问父线程设置的值的情况下使用。例如,在Web应用程序中,可以使用InheritableThreadLocal来传递用户身份信息或上下文数据给子线程处理请求。
  3. 子线程可修改值:与ThreadLocal不同,InheritableThreadLocal允许子线程修改从父线程继承的值。这意味着子线程可以在继承的值的基础上进行修改,而不会影响其他线程的副本。

        使用InheritableThreadLocal与ThreadLocal类似,可以通过set()方法设置值,在子线程中使用get()方法获取值。以下是一个简单的示例:

public class InheritableThreadLocalExample {private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();public static void main(String[] args) throws InterruptedException {inheritableThreadLocal.set("Parent Thread Value");Thread childThread = new Thread(() -> {String value = inheritableThreadLocal.get();System.out.println("Child Thread Value: " + value);inheritableThreadLocal.set("Modified Child Thread Value");value = inheritableThreadLocal.get();System.out.println("Child Thread Modified Value: " + value);});childThread.start();childThread.join();String parentValue = inheritableThreadLocal.get();System.out.println("Parent Thread Value: " + parentValue);}
}

        在上面的示例中,我们首先在父线程中通过set()方法设置InheritableThreadLocal的值为"Parent Thread Value"。然后创建子线程,在子线程中通过get()方法获取并打印父线程设置的值。接下来,子线程修改InheritableThreadLocal的值为"Modified Child Thread Value",并再次打印该值。最后,父线程再次获取InheritableThreadLocal的值并打印。

        执行该示例将输出以下结果:

Child Thread Value: Parent Thread Value
Child Thread Modified Value: Modified Child Thread Value
Parent Thread Value: Parent Thread Value

        可以看到,子线程成功继承了父线程设置的值,并且可以在子线程中对该值进行修改。此时,父线程仍然保持原来的值

        继承的实现原理:

        在InheritableThreadLocal中,父线程的值通过线程创建过程中的inheritableThreadLocals字段进行传递。当子线程被创建时,它会复制父线程的inheritableThreadLocals字段,以便子线程可以访问和修改父线程设置的值// 父线程中的 InheritableThreadLocal 的值没用被修改

        Thread类中init()方法中的源码分析:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {//省略...}/* checkAccess regardless of whether or not threadgroup isexplicitly passed in. */g.checkAccess();/** Do we have the required permissions?*/if (security != null) {//省略...}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target;setPriority(priority);if (inheritThreadLocals && parent.inheritableThreadLocals != null)// 在这里创建inheritableThreadLocals,复制父类的值this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}

        需要注意的是,InheritableThreadLocal的继承是单向的。子线程可以访问和修改父线程中的值,但父线程无法访问或修改子线程中的值。每个线程在自己的上下文中独立地维护自己的InheritableThreadLocal副本。

        InheritableThreadLocal在某些情况下非常有用,特别是在需要将上下文信息传递给子线程的场景中。例如,在Web应用程序中,可以在主线程中设置用户身份信息,然后通过InheritableThreadLocal将该信息传递给子线程,以便子线程可以使用该信息进行授权或其他操作。

        // 需要注意的是,InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果,因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。

        下边是demo:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestInheritableThreadLocalAndExecutor implements Runnable {private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();private static ExecutorService executorService = Executors.newFixedThreadPool(1);public static void main(String[] args) throws Exception{System.out.println("----主线程启动");inheritableThreadLocal.set("主线程第一次赋值");System.out.println("----主线程设置后获取值:" + inheritableThreadLocal.get());executorService.submit(new TestInheritableThreadLocalAndExecutor());System.out.println("主线程休眠2秒");Thread.sleep(2000);inheritableThreadLocal.set("主线程第二次赋值");executorService.submit(new TestInheritableThreadLocalAndExecutor());executorService.shutdown();}@Overridepublic void run() {System.out.println("----子线程获取值:" + inheritableThreadLocal.get());}
}

        运行结果:

----主线程启动
----主线程设置后获取值:主线程第一次赋值
主线程休眠2秒
----子线程获取值:主线程第一次赋值
----子线程获取值:主线程第一次赋值

        从上图可以看出,我们在main线程中第二次set并没有被第二次submit的线程get到。也印证了我们的结论。// 从这一点来看InheritableThreadLocal的功能比较鸡肋

        推荐阅读,《线程池如何传递上下文信息》。

这篇关于Java 中的线程局部变量ThreadLocal和InheritableThreadLocal分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/758148

相关文章

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@

springboot循环依赖问题案例代码及解决办法

《springboot循环依赖问题案例代码及解决办法》在SpringBoot中,如果两个或多个Bean之间存在循环依赖(即BeanA依赖BeanB,而BeanB又依赖BeanA),会导致Spring的... 目录1. 什么是循环依赖?2. 循环依赖的场景案例3. 解决循环依赖的常见方法方法 1:使用 @La

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

Elasticsearch 在 Java 中的使用教程

《Elasticsearch在Java中的使用教程》Elasticsearch是一个分布式搜索和分析引擎,基于ApacheLucene构建,能够实现实时数据的存储、搜索、和分析,它广泛应用于全文... 目录1. Elasticsearch 简介2. 环境准备2.1 安装 Elasticsearch2.2 J

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("