Java并发编程(七)—ThreadLocal的原理及应用详解

news2024/9/21 22:33:01

目录

一、ThreadLocal的原理

1、ThreadLocal对象

2、ThreadLocalMap

3、Thread 对象

4、get() 和 set() 方法

5、内存管理

二、ThreadLcoal的应用

三、ThreadLocal扩展问题

四、总结


ThreadLocal 类在 Java 中提供了一种机制,可以在每个线程中存储独立的变量副本,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用。避免了多线程环境下的数据共享问题

//创建一个ThreadLocal变量
private static final ThreadLocal<String> requestContext = new ThreadLocal<>();

并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现线性安全问题

为了解决线性安全问题,可以用加锁的方式,比如使用synchronized 或者Lock。但是加锁的方式,可能会导致系统变慢,也可以使用volatile

一、ThreadLocal的原理

先了解一下ThreadLocal的结构图

再瞅一眼ThreadLocal的源码结构:

public class ThreadLocal<T> {
     ...
     public T get() {
        ...
    }
    
    //用于设置 ThreadLocal 的初始值
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        ...
    }
    
     static class ThreadLocalMap {
        ...
     }
     ...
}

再一个个细看内部元素的含义:

1、ThreadLocal对象

ThreadLocal 是一个类,它定义了如何为每个线程创建和存储变量副本的方法。

当创建一个 ThreadLocal 对象时,它本身并不存储任何数据,而是为每个使用它的线程提供了一个独立的数据槽。

2、ThreadLocalMap

在源码中,有一个静态的内部类叫:ThreadLocalMap,它存储了该线程的所有 ThreadLocal 变量的副本

它是一个哈希表,用于存储键值对,其中ThreadLocal 对象是线程的局部变量副本

也就是一个Entry数组,其中:Entry = ThreadLocal + value

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

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

3、Thread 对象

之前说了ThreadLocal为每个线程提供了本地变量,那Thread中肯定是有ThreadLocal的,见Thread源码:

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

由此可见:在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据

4、get() 和 set() 方法

ThreadLocal 提供了 get() 方法来获取当前线程的局部变量副本,set() 方法用于设置当前线程的局部变量副本的值

public class ThreadLocal<T> {
     ...
     public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }
    
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     ...
}

5、内存管理

ThreadLocalMap 使用 WeakReference 作为键来存储 ThreadLocal 对象,ThreadLocal对象是弱引用,以防止内存泄漏。

如果 ThreadLocal 对象没有被引用,在GC的时候,那么它会被垃圾回收器回收,而 ThreadLocalMap 中对应的条目也会被自动清理

总结一下引用关系:

除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用

到这里应该了解了一点ThreadLocal,那有几个问题需要思考:

1:为什么用ThreadLocal做key,而不是用Thread做key?

一个线程中只使用了一个ThreadLocal对象,但是一个线程未必只有一个ThreadLocal对象

每个 ThreadLocal 对象都是唯一的,因此可以用作键来区分不同的 ThreadLocal 实例

这种唯一性保证了每个 ThreadLocal 对应的变量副本可以在 ThreadLocalMap 中正确地存储和检索

2:为什么ThreadLocal对象是弱引用,而不是强引用?

先了解一下弱引用强引用

  • 强引用:最常用的引用类型,只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
  • 弱引用:只能通过 WeakReference 类来创建,当 JVM 进行垃圾回收并且需要更多内存时,即使系统中还有足够的内存,弱引用关联的对象也会被回收

从流程图和源码可以知道,Thread中的ThreadLocal变量对ThreadLocal对象是有强引用存在的

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁

就会存在这样的强引用链Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题

因此,ThreadLocal对象是弱引用

二、ThreadLcoal的应用

乍一看,volatileThreadLocal有一点类似,都是为了解决并发场景下修改一个共享变量的问题,那为什么还需要用ThreadLocal呢?


在之前的文章Java并发—volatile关键字的作用及使用场景-CSDN博客中,已经详细讲了volatile

还是使用之前的例子🌰:线程A和线程B从主内存读取和修改x=1的过程:

  • 初始化:创建一个 ThreadLocal 对象,并通过它为每个线程分配一个独立的副本。
  • 读取:线程 A 和 B 分别从各自的 ThreadLocal 实例中读取 x 的值。
  • 修改:线程 A 修改 x 的值时,实际上是在修改自己线程内的副本。
  • 可见性:线程 B 读取 x 的值时,只能看到自己线程内的副本,不会看到线程 A 的修改。

示例代码
下面是一个简单的示例,展示了使用 ThreadLocal 如何为每个线程提供独立的变量副本:

public class VolatileExample {

    private static volatile int x = 1;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.submit(() -> {
            System.out.println("Thread A: Before modification, x = " + x);
            x = 3; // 修改 x 的值
            System.out.println("Thread A: After modification, x = " + x);
        });

        executorService.submit(() -> {
            System.out.println("Thread B: Before modification, x = " + x);
            x = 4; // 修改 x 的值
            System.out.println("Thread B: After modification, x = " + x);
        });

        executorService.shutdown();
    }
}

运行结果:

Thread A: Before modification, x = 1
Thread A: After modification, x = 3

Thread B: Before modification, x = 1
Thread B: After modification, x = 4
  • 线程A:从主内存读取到x=1,修改到x=3
  • 线程B:从主内存读取到x=1,修改到x=4
  • 最终结果:
    • 线程A的副本中的 x 值为 3。
    • 线程B的副本中的 x 值为 4。
    • 主内存中的 x 值仍然为 1,因为 ThreadLocal 并不修改主内存中的值

从运行结果可见:

使用 ThreadLocal 时,每个线程都有一个独立的变量副本,因此线程 A 和 B 读取和修改的实际上是它们自己的副本,而不是共享的主内存中的 x。这种方式避免了线程间的竞争和同步问题,同时也意味着线程 A 对 x 的修改不会影响到线程 B

而使用 volatile 时,线程 A 和线程 B 读取和修改的是同一个共享变量 x

ThreadLocal和volatile的区别:

  • 目的不同:volatile 解决的是变量在多线程间的可见性问题,而 ThreadLocal 解决的是线程间的数据隔离问题。
  • 应用场景不同:volatile 适用于状态标记和简单的原子性操作,而 ThreadLocal 适合于存储每个线程独享的资源或状态信息。
  • 数据共享与否:volatile 变量在所有线程中共享,而 ThreadLocal 中的变量对每个线程来说是独立的

ThreadLocal 是一种非常有用的工具,它可以让每个线程拥有自己的变量副本,从而避免了线程间的共享状态问题,那么ThreadLocal的应用场景:

1、每个线程独享的对象

工具类:例如 SimpleDateFormat 和 Random 等类,这些类在多线程环境下可能会产生线程安全问题。使用 ThreadLocal 可以为每个线程提供独立的实例。

线程池中的工具类:在使用线程池时,确保每个线程都有自己的工具类实例,避免线程间的数据竞争

2、保存线程内的全局变量

拦截器中的用户信息:在 Web 开发中,可以在拦截器中获取用户的认证信息(如用户名、用户ID等),并将其存储在 ThreadLocal 中,以便后续处理中可以直接使用,避免了参数传递的麻烦。

请求上下文信息:在 Web 请求处理过程中,可以将请求相关的上下文信息(如请求ID、事务ID等)存储在 ThreadLocal 中,方便日志记录和跟踪

🌰: ThreadLocal来存储和访问当前登录用户信息

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

public class UserContextHolder {

    // 创建一个 ThreadLocal 变量来存储 UserInfo
     private static final ThreadLocal<UserInfo> userInfoHolder = new ThreadLocal<>();

    /**
     * 设置用户信息
     */
    public static void setUserInfo(UserInfo userInfo) {
        userInfoHolder.set(userInfo);
    }

    /**
     * 获取当前线程的用户信息
     */
    public static UserInfo getUserInfo() {
        UserInfo userInfo = userInfoHolder.get();
        if (userInfo == null) {
            throw new EcholaRuntimeException(SysExceptionEnum.UNAUTHORIZED);
        }
        return userInfo;
    }
    /**
     * 移除用户信息
     */
    public static void removeRequestId() {
        userInfoHolder.remove();
    }
}

在拦截器 (HandlerInterceptor) 中处理 ThreadLocal 的设置和清除,将并用户信息将其设置到 ThreadLocal 中,以确保每个请求都有独立的上下文

public class UserInfoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求中获取用户信息
        // 在请求处理之前调用,用于从请求中提取用户信息,并将其设置到 ThreadLocal 中
        UserInfo userInfo = extractUserInfoFromRequest(request);

        // 设置用户信息到 ThreadLocal
        UserInfo.setUserInfo(userInfo);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 这里可以做些额外的处理,例如添加模型数据
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求处理完成后,清除 ThreadLocal
        UserInfo.remove();
    }

    /**
     * 从请求中提取用户信息
     *
     * @param request 请求对象
     * @return 用户信息
     */
    private UserInfo extractUserInfoFromRequest(HttpServletRequest request) {
        // 假设用户信息存储在请求头中
        String userId = request.getHeader("X-User-ID");
        if (userId == null) {
            throw new LzmRuntimeException(SysExceptionEnum.UNAUTHORIZED);
        }

        // 创建 UserInfo 对象
        UserInfo userInfo = new UserInfo();
        userInfo.setId(Integer.parseInt(userId));
        // ... 其他字段的设置

        return userInfo;
    }
}

这样在处理请求的过程中,无论在哪一层代码中,都可以通过 getUserInfo 方法来获取当前登录用户信息

UserInfo userInfo = UserContextHolder.getUserInfo();

通过这种方式,每个请求都有独立的用户信息上下文,保证了线程之间的数据隔离,同时也确保了资源的正确释放

3、替代参数传递

避免参数传递:在多个方法之间传递相同的参数时,可以使用 ThreadLocal 来存储这些参数,从而避免了在方法签名中显式传递这些参数。

日志记录:在日志记录中,可以使用 ThreadLocal 来存储一些上下文信息(如请求ID、用户ID等),以便在日志中记录这些信息

4、线程池中的上下文管理

线程池中的线程上下文:在使用线程池时,可以使用 ThreadLocal 来存储每个线程的上下文信息,如线程标识、配置信息等

5、数据库连接管理

线程本地的数据库连接:在多线程环境中,为了提高性能,可以使用 ThreadLocal 来管理每个线程的数据库连接,避免了连接的频繁创建和销毁

三、ThreadLocal扩展问题

然而,在使用线程池时,由于线程池中的线程会被复用,因此直接使用 ThreadLocal 可能会导致一些问题,比如数据污染或内存泄漏

❓:那么如何在线程池中共享数据?

1、 使用 InheritableThreadLocal(可继承的ThreadLocal)
InheritableThreadLocal 是 ThreadLocal 的一个子类,它允许子线程继承父线程的 ThreadLocal 值。虽然这在某些情况下有用,但它不适合线程池,因为线程池中的线程通常是复用的,而且通常不会有父子线程关系

2、在线程池的 ThreadFactory 中初始化 ThreadLocal

可以通过覆盖线程池的 ThreadFactory 来为每个新创建的线程初始化 ThreadLocal 的值。这种方式确保每个线程在其生命周期开始时都会获得一个新的初始值

四、总结

ThreadLocal 是 Java 中的一种特殊机制,它为每个线程提供了一个独立的变量副本,主要用于解决多线程环境下的线程安全问题

通过 ThreadLocal,每个线程可以拥有自己的变量副本,从而避免了线程间的数据竞争和同步问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2032996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python】基础语法介绍

目录 一、标识符和关键字 二、注释 三、缩进 四、输入和输出 五、字符串操作 六、基本数据类型 七、复合数据类型 7.1 列表 7.2 元组 7.3 字典 7.4 集合 八、数据类型转换 九、运算符 8.1 算术运算符 8.2 比较运算符 8.3 赋值运算符 8.4 位运算符 8.5 逻辑运…

网络编程day1

一、思维导图 网络基础

wordpress全局自适应网址导航整站打包源码,含主题和数据库

wordpress全局自适应网址导航整站打包源码&#xff0c;含主题和数据库。直接恢复就可以使用了。 这个是自适应的布局设计&#xff0c;体验还不错。用网址导航是可以的。 代码免费下载&#xff1a;百度网盘

golang for range time.Ticker 和 time.Timer时间通道使用示例 - 每隔指定时间执行一次,执行指定时长后退出执行

golang中的 ticker和timer时间通道除了可以使用for select case语句来执行外&#xff0c; 还可以使用 for range语句来执行ticker或者timer时间通道。 for range time.Ticker 和 time.Timer时间通道使用示例 下面的示例演示了time.Ticker 和 time.Timer的区别和使用演示。 Ti…

《向量数据库指南》——向量数据库技术积累与商业机会

一豪:Charles提到了一个关键点,就是RAG技术结合模型对非结构化数据的理解和搜索能力,甚至可以很好地架接在传统结构化数据的解决方案中。作为向量数据库的核心技术点,对数据本身特别是非结构化数据的向量化、精炼和压缩,我相信Zilliz等公司有很多独门技巧和技术积累。随着…

UE基础 —— 编辑器界面

菜单栏 UE中每个编辑器都有一个菜单栏&#xff0c;部分菜单会出现在所有编辑器窗口中&#xff0c;如File、Window、Help&#xff0c;其他则是其编辑器特有的&#xff1b; 主工具栏 UE中部分最常用的工具和命令的快捷方式&#xff1b; 1&#xff0c;保存按钮&#xff08;ctrls&a…

NIO线程模型

NIO线程模型主要涉及以下几个方面&#xff1a; 一、基本概念 NIO&#xff08;New Input/Output&#xff09;是Java的一种新的输入输出模型&#xff0c;也被称为非阻塞IO。其核心特点是数据读写操作均是非阻塞的&#xff0c;即在进行读写操作时&#xff0c;若数据未准备好&…

Python第三方库——mrjob的介绍

一、简介 mrjob 是一个强大的 Python 库&#xff0c;它允许开发者以 Pythonic 的方式编写 MapReduce 作业&#xff0c;并在多种环境下运行这些作业&#xff0c;包括本地机器、Hadoop 集群、Amazon Elastic MapReduce (EMR) 和 Google Cloud Dataproc。通过使用 mrjob&#xff…

ARTS Week 37

Algorithm 本周的算法题为 1232. 缀点成线 给定一个数组 coordinates &#xff0c;其中 coordinates[i] [x, y] &#xff0c; [x, y] 表示横坐标为 x、纵坐标为 y 的点。请你来判断&#xff0c;这些点是否在该坐标系中属于同一条直线上。 示例 1:输入&#xff1a;coordinates …

8月9日笔记

8月9日笔记 什么是代理? “代理”通常指的是“网络代理”&#xff0c;它是一种特殊的网络服务&#xff0c;允许一个网络终端&#xff08;一般为客户端&#xff09;通过这个服务与另一个网络终端&#xff08;一般为服务器&#xff09;进行非直接的连接。代理服务器作为中间人…

【中项】系统集成项目管理工程师-第11章 项目范围管理-11.3定义范围

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

PHP利用PCRE回溯次数

目录 原理 例子 来一道题&#xff08;2018i春秋圣诞欢乐赛官方WriteUp&#xff09; 利用php弱语言特性解题 利用回溯 原理 对于一串正则表达式来说它匹配了一系列的字符串后自身的正则还没有用完&#xff0c;这个时候就会触发回溯机制&#xff0c;超过回溯次数正则匹配就失…

【代码故事】VSCode知名主题material-theme仓库代码清空

大家好&#xff0c;我是前端之虎陈随易。 这是我的个人网站 https://chensuiyi.me。 出大事了 看到了一篇前端社区开源扛把子 Anthony Fu 的帖子。 经过一番了解&#xff0c;出大事了&#xff01; 知名 VSCode 主题 material-theme 仓库清空了&#xff01; 连带着所有提交…

【MySQL】1.MySQL基本操作

目录 一、MySQL数据库登陆 1、设置环境变量 2、cmd命令登陆数据库 二、基本操作语法 1、显示数据库——SHOW 2、使用/选择数据库——USE 3、删除——DROP 4、创建——CREATE 5、查看表结构——DESC 6、数据操作——增删改查 &#xff08;1&#xff09;增/插入&#…

SpringCloud-gateway编码实现路由策略的自动刷新,动态路由

文章目录 一、概述1、背景2、实现思路 二、编码实现1、nacos配置刷新公共类2、自定义RouteDefinition3、route缓存类4、动态更新路由网关service5、动态路由加载类 三、测试 一、概述 1、背景 gateway可以配置路由断言过滤器&#xff0c;但是通常一个微服务体系下&#xff0c…

KCTF 闯关游戏:1 ~ 7 关

前言 看雪CTF平台是一个专注于网络安全技术竞赛的在线平台&#xff0c;它提供了一个供网络安全爱好者和技术专家进行技术交流、学习和竞技的环境。CTF&#xff08;Capture The Flag&#xff0c;夺旗赛&#xff09;是网络安全领域内的一种流行竞赛形式&#xff0c;起源于1996年…

虚拟机Linux系统字体太小怎么办?

每次用虚拟机的Linux系统时&#xff0c;都觉得字体小得伤眼睛&#xff0c;所以就尝试找了下&#xff0c;没想到可以直接用大号字体&#xff0c;这感觉好多啦~ 这里针对centOS的图形界面&#xff0c;非常简单&#xff0c;见下面的图&#xff1a; 应用程序 --> 系统工具 --&…

ELK三个开源软件的工作原理

一、Elasticsearch Elasticsearch的工作原理主要涉及其数据处理、索引机制、查询过程以及集群管理等方面。一下是对Elasticsearch工作原理的详细解析&#xff1a; 1. 数据处理 1.1 数据导入 Elasticsearch支持多种数据源&#xff0c;包括直接输入、通过Logstash和Beats等工具…

js日期处理库--dayjs

js中处理日期是一件比较麻烦的事情&#xff0c;这里推荐使用day.js库来处理&#xff0c;文档:Day.js中文网 引入库 如果没有引入过dayjs,需要先执行npm install,然后import就能使用了 npm install dayjs import dayjs from dayjsconsole.log(dayjs().format()) 如果使用的…

leetcode日记(67)单词搜索

太坑了&#xff01;老是时间超限&#xff0c;不是时间超限就是内存超限&#xff01; 思路很简单&#xff0c;就是先遍历整个网格寻找开头&#xff0c;然后上下左右搜寻找下一个字母&#xff0c;引用递归。 最终看边答案边写出来的&#xff1a; class Solution { public:bool …