首页 > 编程语言 >【Java】ThreadLocal详解

【Java】ThreadLocal详解

时间:2024-11-01 16:17:33浏览次数:3  
标签:map Java Thread ThreadLocalMap value ThreadLocal 详解 线程

引言

在多线程编程中,如何安全地共享数据是一个重要的课题。Java 提供了 ThreadLocal 类,以便在每个线程中维护线程局部变量,允许每个线程拥有自己的独立变量副本。本文将探讨 ThreadLocal 的工作原理、使用场景以及一些最佳实践。

1. 什么是 ThreadLocal?

ThreadLocal 是 Java 提供的一个类,它用于创建线程局部变量。每个线程在访问 ThreadLocal 变量时,都会获取到一个与其他线程隔离的副本。这意味着,一个线程对 ThreadLocal 变量的修改不会影响其他线程。

当你创建一个 ThreadLocal 变量并调用 get() 方法时,会为当前线程创建一个新的副本(如果它之前没有)。每个线程都有一个 ThreadLocalMap,其中存储了所有 ThreadLocal 变量的值。这个映射只在当前线程的上下文中可见,从而实现了数据的隔离。

Thread、ThreadLocalMap、ThreadLocal结构关系图如下:
在这里插入图片描述

  • 每个Thread都有一个ThreadLocalMap变量
    在这里插入图片描述

  • ThreadLocalMap内部定义了Entry(ThreadLocal<?> k, Object v)节点类,在 Entry中,ThreadLocal 实例作为键(key),而通过 set(value) 方法存储的值作为value
  • 这个Entry节点继承了WeakReference类泛型为ThreacLocal类
    在这里插入图片描述

2. ThreadLocal主要方法解析

2.1 ThreadLocal 的 set 方法

set方法的源码如下:

/**
 * 为当前 ThreadLocal 对象关联 value 值
 * @param value 要存储在此线程的线程副本的值
 */
public void set(T value) {
	// 返回当前ThreadLocal所在的线程
	Thread t = Thread.currentThread();
	// 返回当前线程持有的map
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对
		map.set(this, value);
	} else {
		// 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
		createMap(t, value);
	}
}

set 方法主要流程为:

  • 先获取到当前线程的引用
  • 利用这个引用来获取到 ThreadLocalMap
  • 如果 map 为空,则去创建一个 ThreadLocalMap
  • 如果 map 不为空,就利用 ThreadLocalMap 的 set 方法将 value 添加到 map 中

2.2 ThreadLocal 的 get 方法

get方法的源码如下:

/**
 * 返回当前 ThreadLocal 对象关联的值
 *
 * @return
 */
public T get() {
	// 返回当前 ThreadLocal 所在的线程
	Thread t = Thread.currentThread();
	// 从线程中拿到 ThreadLocalMap
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		// 从 map 中拿到 entry
		ThreadLocalMap.Entry e = map.getEntry(this);
		// 如果不为空,读取当前 ThreadLocal 中保存的值
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T) e.value;
			return result;
		}
	}
	// 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,初始化的值为null
	return setInitialValue();
}

get 方法的主要流程为:

  • 先获取到当前线程的引用
  • 获取当前线程内部的 ThreadLocalMap
  • 如果 map 存在,则获取当前 ThreadLocal 对应的 value 值
  • 如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化

2.3 ThreadLocal 的 remove 方法

remove方法的源码如下:

/**
 * 清理当前 ThreadLocal 对象关联的键值对
 */
public void remove() {
	// 返回当前线程持有的 map
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null) {
		// 从 map 中清理当前 ThreadLocal 对象关联的键值对
		m.remove(this);
	}
}

remove方法的主要流程为:

  • 先获取到当前线程的 ThreadLocalMap,并且调用了它的 remove 方法
  • 然后从 map 中清理当前 ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了

3. ThreadLocal内存泄漏问题

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    // 其他方法...
    
}
  • 由ThreadLocalMap 的源码我们知道,ThreadLocal 实例存储在 ThreadLocalMap 的Entry中,Entry的key为ThreadLocal对象的弱引用,但是映射值(即 Entry 中的value)是强引用。

  • 当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了 ThreadLoca l对象的强引用,即表示该 ThreadLocal 对象可以被回收了。又因为Entry中key为ThreadLocal对象的弱引用,所以当JVM执行GC操作时是能够回收该ThreadLocal对象的。

  • 而Entry中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal对象,是无法释放ThreadLocalMap中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。

因此,在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。

例子:

public class ThreadLocalExample {

    // 创建一个 ThreadLocal 变量
    private static ThreadLocal<String> threadLocalValue = ThreadLocal.withInitial(() -> "Initial Value");

    public static void main(String[] args) {
        // 创建多个线程来演示 ThreadLocal 的使用
        Runnable task = () -> {
            // 设置 ThreadLocal 的值
            threadLocalValue.set(Thread.currentThread().getName() + " - ThreadLocal Value");

            // 获取并打印 ThreadLocal 的值
            System.out.println(threadLocalValue.get());

            // 重要:使用完后清理 ThreadLocal 的值
            threadLocalValue.remove();
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);

        thread1.start();
        thread2.start();
        thread3.start();

        // 等待线程执行完成
        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 执行结果:
 Thread-0 - ThreadLocal Value
 Thread-1 - ThreadLocal Value
 Thread-2 - ThreadLocal Value
**/

4. ThreadLocal应用场景

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 方便同一个线程使用某一对象,避免不必要的参数传递
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal

标签:map,Java,Thread,ThreadLocalMap,value,ThreadLocal,详解,线程
From: https://blog.csdn.net/weixin_50591390/article/details/143429359

相关文章

  • 二:java 基础知识(2)-- 初始java/语法基础
    目录idea中文插件第一个Java程序Java数据类型,常量与变量1.数据类型    1.1基本数据类型    1.2引用数据类型2.常量2.1特性2.2 定义常量​编辑3.变量        3.1 变量的定义与初始化        3.2变量的类型局部变量:在......
  • Java 在金融科技(FinTech)中的应用
    在探讨Java在金融科技(FinTech)中的应用时,首先要明白Java由于其高性能、安全性、可移植性等特性,在金融行业中占据了举足轻重的地位。这些特点使得Java成为开发高频交易系统、风险管理平台、数据处理框架等金融应用的首选语言。尤其是在安全性方面,Java提供了一套严格的安全机制,包......
  • Java 字符流详解
    在Java的I/O体系中,字符流(Reader和Writer)是专门用于处理文本数据的输入输出流。与字节流不同,字符流以字符为单位进行读取和写入,能够更好地处理文本信息,尤其是包含多字节字符(如中文)的文本文件。本文将从字符流的类关系图开始,详细介绍字符流的原理、使用方法以及常见问题......
  • Java - 手写识别; 如何用spring ai和大模型做手写识别教程
    识别后的文字利用大模型提升Java手写识别:更简单、更高效在Java场景中,我们经常需要处理手写识别的问题。过去,这类需求主要依赖于OCR技术,但其效果并不总是稳定。随着大模型的发展,使用大模型进行java手写识别成为了一种更优的选择。通过引入先进的大模型,不仅提高了识别的准......
  • java.sql.SQLException: ORA-00971: 缺失 SET 关键字
    目录背景:过程:错误原因: 解决办法:总结:背景:正在运行某个项目程序,提交信息之后发现库中并没有刚刚的相关数据,后来查看后台信息,发现提示错误,java.sql.SQLException:ORA-00971:缺失SET关键字,下面一片红色,经过筛选,我看到(UserManage.java:194),显然是UserManage类里面的......
  • 2024最新IntelliJ IDEA常用的小技巧汇总,JAVA 新手上路必备
    目录一、IntelliJIDEA概述二、下载与安装2.1下载2.2安装三、快速创建并运行Java工程3.1创建Java工程3.2创建package和class四、详细设置4.1字体大小设置4.2字符编码设置4.3大小写不敏感设置4.4自动导包4.5启动退出设置4.6自动更新五、快速开发5.1代码模板......
  • WebSocket详解:从前端到后端的全栈理解
    文章目录前言一、WebSocket简介1.1WebSocket的特点二、WebSocket的工作原理2.1握手过程2.2数据传输三、WebSocket在前端的应用四、WebSocket在后端的应用五、WebSocket的局限与解决方案结语前言随着互联网技术的发展,传统的HTTP协议在某些场景下的局限性逐渐显......
  • java--标识符、常量、变量、类型 转换
    1、注释(增强代码可读性)java中的注释单行注释(“//”)多行注释(/**/)tips:多行注释不能嵌套,否则会报错文档注释(/***/)源代码文件(Xxxx.java)通过编译生成字节码文件(Xxxx.class)的过程中编译器会忽略掉源码中的注释部分2、关键字(赋予特定含义的单词)特点组成关键字的字母全部小......
  • 【日常记录-Java】应用引入Slf4J
    1.简介    SLF4J(SimpleLoggingFacadeforJava)是Java的一个简单日志门面,为Java日志访问提供了一套标准、规范的API框架。而具体日志的实现则可以根据这套接口去实现具体的日志框架,以便将来需要更换日志框架时,只替换实现框架即可。常见的具体实现有JUL、log4j、......
  • 【日常记录-Java】SLF4J扫描实现框架的过程
    1.简介    SLF4J(SimpleLoggingFacadeforJava)作为一种简单的门面或抽象,服务于其他各种日志框架,例如JUL、log4j、logback等,核心作用有两项:提供日志接口;提供获取具体日志对象的方法;2.扫描过程 2.1引入依赖    在使用SLF4J时,需要引入其API依赖以及......