Java线程中的ThreadLocal原理及源码解析

 更新时间:2023年12月07日 10:19:18   作者:外星喵  
这篇文章主要介绍了Java线程中的ThreadLocal原理及源码解析,ThreadLocal 的作用是为每个线程保存一份局部变量的引用,实现多线程之间的数据隔离,从而避免了线程不安全情况的发生,需要的朋友可以参考下

ThreadLocal介绍

ThreadLocal,线程本地变量,ThreadLocal 的作用是为每个线程保存一份局部变量的引用,实现多线程之间的数据隔离,从而避免了线程不安全情况的发生。这个变量保存的值只在线程的生命周期内起作用,通过使用它减少了将执行上下文信息传递到每个方法的需要。

如果多个线程同时在一个对象/实例上执行,它们将共享这个实例变量,如果不使用ThreadLocal,就需要在每个方法上传递参数,去跨对象共享这些变量,同时还会导致线程不安全的问题。

许多框架使用 ThreadLocals 来维护与当前线程相关的一些上下文。例如,当前事务存储在 ThreadLocal 中时,您不需要通过每个方法调用将其作为参数传递,以防堆栈中的某个人需要访问它。Web 应用程序可能会将有关当前请求和会话的信息存储在 ThreadLocal 中,以便应用程序可以轻松访问它们。

ThreadLocal 原理

ThreadLocals 是一种全局变量(尽管由于它们仅限于一个线程而稍微不那么邪恶),因此在使用它们时应该小心以避免不必要的副作用和内存泄漏。

每个Thread对象,专门用一个ThreadLocalMap来存储自己的私有对象。ThreadLocalMap实际上就跟我们常用的HashMap类似,存储在那里的Key-Value形式的数据。

ThreadLocal在每次获取或设置操作时,都先通过Thread.currentThread()方法来获取当前线程,再从当前线程中获取ThreadLocalMap。而实际上,保存的值是通过ThreadLocalMap来存储的。

ThreadLocal对象可以是多线程共享,但ThreadLocalMap对象却是一个线程独享的,每个线程对象,创建一个自己专属的ThreadLocalMap,与其他Thread对象创建的ThreadLocalMap不存在一个单一的关系。

当多个Thread对象共同访问同一个ThreadLocal对象时,threadLocal只是作为ThreadLocalMap的Key存在,而不是作为变量的存储位置。threadLocal的set(方法和get()方法涉及的值是存储为ThreadLocalMap的值而ThreadLocalMap是每个线程专属的,互不相同的。这就是为什么同ThreadLocal被多线程同时访问,ThreadLocal的值却互不干扰的原理。

ThreadLocalMap

ThreadLocalMap该类的核心部分是Entry class,它扩展了WeakReference. 它确保如果当前线程退出,它将被自动垃圾收集。这就是为什么它使用ThreadLocalMap而不是简单的HashMap. 它将当前ThreadLocal及其值作为Entry类的参数传递,所以当我们想要获取值时,我们可以从 中获取它table.

  • 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中, 各管各的,线程可以正确的访问到自己的对象。
  • 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的 ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取 得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
  • ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal使用场景

代替参数的显式传递

当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了 很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

全局存储用户信息

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。 当请求到来时,可以将当前Session信息存储在ThreadLocal中,在请求处理过程中可以随时使用Session信息,每个请求之间的Session信息互不影响。当请求处理完成后通过remove方法将当前Session信息清除即可。

解决线程安全问题

在Spring的Web项目中,我们通常会将业务分为Controller层,Service层,Dao层, 我们都知道@Autowired注解默认使用单例模式,那么不同请求线程进来之后,由于Dao层使用单例,那么负责数据库连接的Connection也只有一个, 如果每个请求线程都去连接数据库,那么就会造成线程不安全的问题,Spring是如何解决这个问题的呢?

在Spring项目中Dao层中装配的Connection肯定是线程安全的,其解决方案就是采用ThreadLocal方法,当每个请求线程使用Connection的时候, 都会从ThreadLocal获取一次,如果为null,说明没有进行过数据库连接,连接后存入ThreadLocal中,如此一来,每一个请求线程都保存有一份 自己的Connection。于是便解决了线程安全问题

ThreadLocal源码

以下是ThreadLocal的get()、set()、remove()方法的代码

/** 
 * 返回当前线程的 this 副本中的值 
 * 线程局部变量。如果变量没有值 
 * 当前线程,首先初始化为返回值 
 * 通过调用 {@link #initialValue} 方法。 
 * 
 * @return 这个线程本地的当前线程的值 
 */  
public T get() {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        ThreadLocalMap.Entry e = map.getEntry(this);  
        if (e != null)  
            return (T)e.value;  
    }  
    return setInitialValue();  
}  
/** 
 * 设置这个线程局部变量的当前线程的副本 
 * 到指定值。大多数子类将不需要  
 * 重写此方法,仅依赖于 {@link #initialValue} 
 * 设置线程局部变量值的方法。 
 * 
 * @param value 要存储在当前线程的副本中的值。
 */  
public void set(T value) {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        map.set(this, value);  
    else  
        createMap(t, value);  
}  
/** 
 * 删除此线程本地的当前线程的值 
 * 多变的。如果此线程局部变量随后 
 * {@linkplain #get read} 被当前线程读取,其值为 
 * 通过调用其 {@link #initialValue} 方法重新初始化, 
 * 除非它的值是当前线程的 {@linkplain #set set} 
 * 在过渡期。这可能会导致多次调用 
 * 当前线程中的 <tt>initialValue</tt> 方法。 
 * 
 * @自 1.5 
 */  
 public void remove() {  
     ThreadLocalMap m = getMap(Thread.currentThread());  
     if (m != null)  
         m.remove(this);  
 }  

ThreadLocal内存溢出问题

内存溢出问题模拟

在执行main方法前,先使用“-Xmx50m”的参数来配置一下 Idea,它表示将程序运行的最大内存设置为 50m,如果程序的运行超过这个值就会出现内存溢出的问题

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalOOMExample {
    /**
     * 定义一个 10m 大的类
     */
    static class MyTask {
        // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }
    // 定义 ThreadLocal
    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
    // 主测试代码
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5, 5, 60,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // 执行 10 次调用
        for (int i = 0; i < 10; i++) {
            // 执行任务
            executeTask(threadPoolExecutor);
            Thread.sleep(1000);
        }
    }
    /**
     * 线程池执行任务
     * @param threadPoolExecutor 线程池
     */
    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
        // 执行任务
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("创建对象");
                // 创建对象(10M)
                MyTask myTask = new MyTask();
                // 存储 ThreadLocal
                taskThreadLocal.set(myTask);
                // 将对象设置为 null,表示此对象不在使用了
                myTask = null;
            }
        });
    }
}

原因分析

由于每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中。而ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。

也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。

解决方案

严格来讲内存溢出并不是 ThreadLocal 的问题,而是因为没有正确使用 ThreadLocal 所带来的问题。想要避免 ThreadLocal 内存溢出的问题,只需要在使用完 ThreadLocal 后调用 remove 方法即可。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class App {
    /**
     * 定义一个 10m 大的类
     */
    static class MyTask {
        // 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }
    // 定义 ThreadLocal
    private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
    // 测试代码
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor =
                new ThreadPoolExecutor(5, 5, 60,
                        TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
        // 执行 n 次调用
        for (int i = 0; i < 10; i++) {
            // 执行任务
            executeTask(threadPoolExecutor);
            Thread.sleep(1000);
        }
    }
    /**
     * 线程池执行任务
     * @param threadPoolExecutor 线程池
     */
    private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
        // 执行任务
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("创建对象");
                try {
                    // 创建对象(10M)
                    MyTask myTask = new MyTask();
                    // 存储 ThreadLocal
                    taskThreadLocal.set(myTask);
                    // 其他业务代码...
                } finally {
                    // 释放内存
                    taskThreadLocal.remove();
                }
            }
        });
    }
}

到此这篇关于Java线程中的ThreadLocal原理及源码解析的文章就介绍到这了,更多相关ThreadLocal原理及源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • maven deploy时报错的解决方法

    maven deploy时报错的解决方法

    这篇文章主要介绍了maven deploy时报错的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java如何实现可折叠Panel方法示例

    Java如何实现可折叠Panel方法示例

    这篇文章主要给大家介绍了关于利用Java如何实现可折叠Panel的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用java具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-07-07
  • Java中JDK14的新特性之JFR,JMC和JFR事件流(推荐)

    Java中JDK14的新特性之JFR,JMC和JFR事件流(推荐)

    JFR是一个基于事件的低开销的分析引擎,具有高性能的后端,可以以二进制格式编写事件,而JMC是一个GUI工具,用于检查JFR创建的数据文件。本文给大家介绍Java中JDK14的新特性之JFR,JMC和JFR事件流的相关知识,感兴趣的朋友一起看看吧
    2020-05-05
  • Java.toCharArray()和charAt()的效率对比分析

    Java.toCharArray()和charAt()的效率对比分析

    这篇文章主要介绍了Java.toCharArray()和charAt()的效率对比分析,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-10-10
  • Java中Thread类基本用法详解

    Java中Thread类基本用法详解

    Java中的Thread类是用于创建和管理线程的类,Thread类提供了许多方法来管理线程,包括启动线程、中断线程、暂停线程等,下面这篇文章主要给大家介绍了关于Java中Thread类基本用法的相关资料,需要的朋友可以参考下
    2023-06-06
  • Java实现规则几何图形的绘制与周长面积计算详解

    Java实现规则几何图形的绘制与周长面积计算详解

    随着计算机的发展,人们对图形的计算要求会越来越高。在各行各业中的计算人员会对图形的计算要有便利的要求,规则几何图形问题求解程序应运而生!本文将用Java编写一个程序,可以实现规则几何图形的绘制与周长面积计算,感兴趣的可以了解一下
    2022-07-07
  • Java数据结构与算法学习之循环链表

    Java数据结构与算法学习之循环链表

    循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。本文将为大家详细介绍一下循环链表的特点与使用,需要的可以了解一下
    2021-12-12
  • Java Swing组件文件选择器JFileChooser简单用法示例

    Java Swing组件文件选择器JFileChooser简单用法示例

    这篇文章主要介绍了Java Swing组件文件选择器JFileChooser简单用法,结合实例形式分析了Swing组件中的文件选择器JFileChooser的简单使用方法,需要的朋友可以参考下
    2017-11-11
  • Spring Cache相关知识总结

    Spring Cache相关知识总结

    今天带大家学习Spring的相关知识,文中对Spring Cache作了非常详细的介绍,对正在学习Java Spring的小伙伴们很有帮助,需要的朋友可以参考下
    2021-05-05
  • 详解Mybatis的分页插件

    详解Mybatis的分页插件

    这篇文章主要介绍了详解Mybatis的分页插件,在 Mybatis中,如何对数据进行分页是一个非常常见的问题,现在,我们可以通过使用 Mybatis 的分页插件来实现对数据的分页,需要的朋友可以参考下
    2023-05-05

最新评论