线程池(六):ThreadLocal相关知识详解

news2025/4/28 14:33:17

线程池(六):ThreadLocal相关知识详解

  • 线程池(六):ThreadLocal相关知识详解
    • 一、概述
      • 定义与作用
      • 应用场景
    • 二、ThreadLocal基本使用
      • 创建ThreadLocal对象
      • 设置和获取值
      • 初始化值
      • 完整示例
    • 三、ThreadLocal的实现原理&源码解析
      • 内部结构
      • set方法源码解析
      • get方法源码解析
      • remove方法源码解析
      • 弱引用的作用
    • 四、ThreadLocal内存泄露问题
      • 内存泄露产生原因
      • 如何避免内存泄露

线程池(六):ThreadLocal相关知识详解

一、概述

定义与作用

ThreadLocal是Java中的一个类,它提供了线程本地变量的功能。简单来说,每个使用ThreadLocal的线程都拥有自己独立的变量副本,各个线程之间的变量副本相互隔离,互不干扰。这就解决了多线程环境下变量共享的冲突问题。

在多线程编程中,当多个线程同时访问一个共享变量时,可能会出现数据不一致等并发问题。而ThreadLocal可以让每个线程都有自己专属的变量,就好像每个线程都“私藏”了一份变量,各自使用各自的,从根本上避免了线程间对共享变量的竞争。

应用场景

  1. 数据库连接:在一个Web应用中,每个请求通常由一个线程来处理。为了避免多个线程之间数据库连接的混乱,我们可以使用ThreadLocal来为每个线程保存一个独立的数据库连接。这样每个线程在处理请求过程中,使用的都是自己的数据库连接,不会相互干扰。
  2. 用户会话信息:在Web开发中,需要记录当前用户的会话信息,比如用户的登录状态、用户ID等。使用ThreadLocal可以方便地在一个线程处理的整个流程中随时获取和设置这些会话信息,并且不同用户的请求线程之间的会话信息不会混淆。
  3. 事务管理:在涉及事务的操作中,确保一个事务内的一系列操作都在同一个数据库连接上进行是很重要的。通过ThreadLocal可以将事务相关的数据库连接绑定到当前线程,在事务的各个操作环节中,都能使用到同一个连接,保证事务的一致性。

二、ThreadLocal基本使用

创建ThreadLocal对象

创建ThreadLocal对象非常简单,只需要使用new关键字即可。示例代码如下:

ThreadLocal<String> threadLocal = new ThreadLocal<>();

这里创建了一个ThreadLocal对象,它存储的是String类型的变量。

设置和获取值

  1. 设置值:通过set方法可以为当前线程设置ThreadLocal变量的值。示例如下:
threadLocal.set("Hello, ThreadLocal!");

这行代码会将字符串"Hello, ThreadLocal!"设置为当前线程对应的ThreadLocal变量的值。

  1. 获取值:使用get方法可以获取当前线程中ThreadLocal变量的值。示例如下:
String value = threadLocal.get();
System.out.println(value);

如果当前线程还没有设置过值,get方法会返回null

初始化值

有时候我们希望在ThreadLocal被创建时就有一个初始值,而不是等到第一次set操作。可以通过继承ThreadLocal并重写initialValue方法来实现。示例代码如下:

ThreadLocal<String> initializedThreadLocal = new ThreadLocal<>() {
    @Override
    protected String initialValue() {
        return "Initial value";
    }
};
String initialValue = initializedThreadLocal.get();
System.out.println(initialValue);  // 输出:Initial value

或者使用Java 8引入的withInitial方法:

ThreadLocal<String> anotherThreadLocal = ThreadLocal.withInitial(() -> "Another initial value");
String anotherInitValue = anotherThreadLocal.get();
System.out.println(anotherInitValue);  // 输出:Another initial value

完整示例

下面是一个完整的示例,展示了多个线程使用ThreadLocal的情况:

public class ThreadLocalExample {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal.set(10);
            System.out.println("Thread1: " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set(20);
            System.out.println("Thread2: " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }
}

在这个示例中,thread1thread2两个线程分别设置并获取自己线程内ThreadLocal变量的值,它们之间互不影响。

三、ThreadLocal的实现原理&源码解析

内部结构

  1. Thread类中的ThreadLocalMap:在Thread类中,有一个名为threadLocals的成员变量,它的类型是ThreadLocal.ThreadLocalMap。这是一个专门为ThreadLocal设计的定制化的哈希映射表。每个线程都有自己独立的ThreadLocalMap,用于存储该线程中所有ThreadLocal变量及其对应的值。
  2. ThreadLocalMap的EntryThreadLocalMap内部使用Entry数组来存储数据。EntryThreadLocalMap的静态内部类,它继承自WeakReference<ThreadLocal<?>>。每个Entry对象包含一个对ThreadLocal对象的弱引用(作为键)和对应的值。

set方法源码解析

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  1. 首先获取当前线程Thread t = Thread.currentThread();,这是因为ThreadLocal变量是与线程绑定的,每个线程都有自己的副本。
  2. 然后通过getMap(t)方法获取当前线程的ThreadLocalMapgetMap方法的实现如下:
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

它直接返回当前线程的threadLocals成员变量,即该线程的ThreadLocalMap
3. 如果ThreadLocalMap不为null,则调用map.set(this, value)将当前ThreadLocal对象(this)作为键,传入的值value作为值,存入ThreadLocalMap中。map.set方法的实现较为复杂,主要是处理哈希冲突等情况,大致思路是通过计算ThreadLocal对象的哈希值,找到对应的数组索引位置,如果该位置已经有元素(发生哈希冲突),则通过线性探测等方式寻找下一个可用的位置来存储。
4. 如果当前线程的ThreadLocalMapnull,则调用createMap(t, value)方法创建一个新的ThreadLocalMap,并将当前ThreadLocal对象和值存入其中。createMap方法的实现如下:

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

它创建了一个新的ThreadLocalMap对象,并将其赋值给当前线程的threadLocals成员变量。

get方法源码解析

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  1. 同样先获取当前线程Thread t = Thread.currentThread();,再获取当前线程的ThreadLocalMap
  2. 如果ThreadLocalMap不为null,则通过map.getEntry(this)方法获取与当前ThreadLocal对象对应的EntrygetEntry方法主要是根据ThreadLocal对象的哈希值在Entry数组中查找对应的元素。
  3. 如果找到了对应的Entry,则将其中存储的值转换为相应类型并返回。
  4. 如果没有找到对应的Entry(可能是因为还没有设置过值等原因),则调用setInitialValue方法。setInitialValue方法的实现如下:
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

它先调用initialValue方法获取初始值(如果之前重写过initialValue方法),然后将这个初始值存入当前线程的ThreadLocalMap中,并返回该初始值。

remove方法源码解析

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
  1. 先获取当前线程的ThreadLocalMap
  2. 如果ThreadLocalMap不为null,则调用m.remove(this)方法从ThreadLocalMap中移除与当前ThreadLocal对象对应的键值对。remove方法会根据ThreadLocal对象的哈希值找到对应的Entry,并将其从数组中移除,同时还会处理一些相关的清理和调整工作,以保证ThreadLocalMap的正常结构和性能。

弱引用的作用

ThreadLocalMap中的Entry继承自WeakReference<ThreadLocal<?>>,这意味着ThreadLocal对象作为键是被弱引用的。当没有其他强引用指向ThreadLocal对象时,在下次垃圾回收时,这个ThreadLocal对象就会被回收。这样设计的好处是可以避免内存泄漏。例如,当一个ThreadLocal对象不再被使用(没有强引用指向它),如果不是弱引用,那么即使线程结束了,ThreadLocalMap中仍然会持有这个ThreadLocal对象的引用,导致该对象无法被回收,从而造成内存泄漏。而使用弱引用,在合适的时候可以让ThreadLocal对象被垃圾回收器回收,减少内存占用。

四、ThreadLocal内存泄露问题

内存泄露产生原因

虽然ThreadLocalMap中对ThreadLocal对象采用了弱引用,在一定程度上可以避免内存泄漏,但如果使用不当,仍然可能会出现内存泄漏问题。主要原因在于Entry中的值(value)是强引用。当ThreadLocal对象被垃圾回收后,Entry中对应的键(ThreadLocal对象的弱引用)变为null,但值(value)仍然存在于ThreadLocalMap中,如果后续没有对这些无效的Entry进行清理,那么这些值就会一直占用内存,导致内存泄漏。

例如,在线程池场景中,线程是复用的。如果一个线程使用了ThreadLocal,并设置了值,当这个线程执行完任务被放回线程池等待下一次任务时,即使ThreadLocal对象本身已经没有强引用(可以被垃圾回收),但线程的ThreadLocalMap中仍然保留着之前设置的值的强引用。如果不进行清理,随着线程池不断复用线程,这些无效的值就会不断累积,占用越来越多的内存。

如何避免内存泄露

  1. 手动调用remove方法:在使用完ThreadLocal变量后,及时调用remove方法。例如在Web应用中,在一个请求处理完成后,在相关的拦截器或者业务代码中调用ThreadLocalremove方法,将当前线程中ThreadLocal变量对应的值从ThreadLocalMap中移除。示例代码如下:
public class MemoryLeakAvoidanceExample {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                threadLocal.set(10);
                // 模拟业务逻辑处理
                // ...
            } finally {
                threadLocal.remove();
            }
        });
        thread.start();
    }
}

在这个示例中,使用finally块确保无论业务逻辑是否正常执行完毕,都会调用remove方法,及时清理ThreadLocal变量对应的值。
2. 使用try - finally代码块:在使用ThreadLocal的代码块中,尽量使用try - finally结构。在try块中进行正常的业务操作,包括设置和获取ThreadLocal变量的值,在finally块中调用remove方法。这样可以保证在任何情况下,即使出现异常,也能正确地清理ThreadLocal变量,避免内存泄漏。
3. 在线程池场景中的特殊处理:对于线程池场景,由于线程会被复用,更需要注意ThreadLocal的清理。可以自定义线程池,在任务执行前设置ThreadLocal变量,在任务执行完成后,通过线程池的钩子方法(如afterExecute方法)来调用ThreadLocalremove方法,确保每次任务执行完毕后都能清理相关的ThreadLocal变量。以下是一个简单的自定义线程池示例:

import java.util.concurrent.*;

public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
    public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 假设这里有一个全局的ThreadLocal变量需要清理
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.remove();
        super.afterExecute(r, t);
    }
}

通过这种方式,可以在多线程复用的场景下有效地避免ThreadLocal导致的内存泄漏问题。

综上所述,ThreadLocal在多线程编程中是一个非常有用的工具,通过合理使用它可以方便地实现线程本地变量的管理,但同时也需要注意其实现原理和可能出现的内存泄漏问题,通过正确的使用方式来确保程序的性能和稳定性。

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

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

相关文章

WSL 中 nvidia-smi: command not found的解决办法

前言 在使用基于 Linux 的 Windows 子系统&#xff08;WSL&#xff09;时&#xff0c;当我们执行某些操作后&#xff0c;可能会遇到输入 nvidia-smi 命令却无法被系统识别的情况。 例如&#xff0c;在终端中输入nvidia-smi 后&#xff0c;系统返回提示 -bash: nvidia-smi: co…

FPGA前瞻篇-组合逻辑电路设计-多路复用器

多路选择器&#xff08;MUX&#xff09;简介 基本概念 多路选择器&#xff08;MUX&#xff0c;Multiplexer&#xff09;是一种多输入、单输出的组合逻辑电路。 它通过选择控制信号&#xff0c;在多个输入信号中选择一个连接到输出端。 可以理解为一个多路数字开关。 &…

【Castle-X机器人】五、物联网模块配置与调试

持续更新。。。。。。。。。。。。。。。 【Castle-X机器人】五、物联网模块配置与调试 五、物联网模块配置与调试5.1 物联网模块调试物联网模块测试:控制物联网模块:物联网模块话题五、物联网模块配置与调试 5.1 物联网模块调试 调试前需确保Castle-x与mqtt主机服务器处于同…

20250426在ubuntu20.04.2系统上打包NanoPi NEO开发板的FriendlyCore系统刷机eMMC的固件

20250426在ubuntu20.04.2系统上打包NanoPi NEO开发板的FriendlyCore系统刷机eMMC的固件 2025/4/26 21:30 缘起&#xff1a;使用NanoPi NEO开发板&#xff0c;编译FriendlyCore系统&#xff0c;打包eMMC固件的时候报错。 1、在ubuntu14.04下git clone异常该如何处理呢&#xff…

JAVA---字符串

ctrlN 搜索界面&#xff08;idea&#xff09; API和API帮助文档 API &#xff1a; 应用程序编程接口&#xff08;换句话说&#xff0c;就是别人已经写好了&#xff0c;我们不需要再编写&#xff0c;直接使用即可&#xff09; Java API &#xff1a;就是JDK中提供的各种功能…

MacOS 10.15上能跑大语言模型吗?

MacOS 10.15上能跑大语言模型吗&#xff1f; 下载安装Ollama运行大语言模型引申出的问题 MacOS 10.15.7&#xff08;发布于2020年9月&#xff09;作为已经发布了将近5年的系统版本能够运行当今流行的大语言模型吗&#xff1f;这篇文章简要介绍了在MacOS 10.15上通过Ollama运行d…

AI Agent开发第37课-DeepSeek的多模态版JanusPro-7B本地安装

开篇 搜遍Janus Pro git issues、谷哥、国内网络,教程全都是错的。因此还是决定写一本全网唯一正确的教程。 目前网上的教程包括外网的教程都是“缺斤少量”,按照那些教程操作下来不是装不起来,就是装起来只能CPU运行,或者运行起来了Janus的Web前端老是转啊转不出内容。 …

神经网络笔记 - 感知机

一 感知机是什么 感知机&#xff08;Perceptron&#xff09;是一种接收输入信号并输出结果的算法。 它根据输入与权重的加权和是否超过某个阈值&#xff08;threshold&#xff09;&#xff0c;来判断输出0还是1。 二.计算方式 感知机的基本公式如下&#xff1a; X1, X2 : …

阿里云基于本地知识库构建RAG应用 | 架构与场景

RAG&#xff08;检索增强生成&#xff0c;Retrieval-Augmented Generation&#xff09;是一种结合了检索和生成技术的框架&#xff0c;旨在通过外部知识库的检索来增强大语言模型&#xff08;LLM&#xff09;的生成能力。 其核心架构包括两个主要部分&#xff1a; 检索模块&a…

CSS简单实用的加载动画、骨架屏有效果图

效果图 .wxml <!-- 骨架屏 --> <view wx:for"{{skeleton}}" wx:key"index" class"container center" style"--w:{{item.w}}rpx;--h:{{item.h}}rpx" /> <!-- 加载 --> <view class"arco-loading center&quo…

3:QT联合HALCON编程—海康相机SDK二次程序开发

思路&#xff1a; 1.定义带UI界面的主函数类 1.1在主函数中包含其它所有类头文件&#xff0c;进行声明和实例化&#xff1b;使用相机时&#xff0c;是用公共相机的接口在某一个具体函数中去实例化具体的海康相机对象。 1.2设计界面&#xff1a;连接相机&#xff0c;单次采集&a…

【前后端分离项目】Vue+Springboot+MySQL

文章目录 1.安装 Node.js2.配置 Node.js 环境3.安装 Node.js 国内镜像4.创建 Vue 项目5.运行 Vue 项目6.访问 Vue 项目7.创建 Spring Boot 项目8.运行 Spring Boot 项目9.访问 Spring Boot 项目10.实现 Vue 与 Spring Boot 联动11.安装 axios12.编写请求13.调用函数请求接口14.…

数据结构和算法(八)--2-3查找树

目录 一、平衡树 1、2-3查找树 1.1、定义 1.2、查找 1.3、插入 1.3.1、向2-结点中插入新键 1.3.2、向一棵只含有一个3-结点的树中插入新键 1.3.3、向一个父结点为2-结点的3-结点中插入新键 1.3.4、向一个父结点为3-结点的3-结点中插入新键 1.3.5、分解根结点 1.4、2…

Unity-Shader详解-其二

前向渲染和延迟渲染 前向渲染和延迟渲染总的来说是我们的两种主要的渲染方式。 我们在Unity的Project Settings中的Graphic界面能够找到渲染队列的设定&#xff1a; 我们也可以在Main Camera这里进行设置&#xff1a; 那这里我们首先介绍一下两种渲染&#xff08;Forward R…

深入浅出理解并应用自然语言处理(NLP)中的 Transformer 模型

1 引言 随着信息技术的飞速发展&#xff0c;自然语言处理&#xff08;Natural Language Processing, NLP&#xff09;作为人工智能领域的一个重要分支&#xff0c;已经取得了长足的进步。从早期基于规则的方法到如今的深度学习技术&#xff0c;NLP 正在以前所未有的速度改变着我…

当自动驾驶遇上“安全驾校”:NVIDIA如何用技术给无人驾驶赋能?

自动驾驶技术的商业化落地&#xff0c;核心在于能否通过严苛的安全验证。国内的汽车企业其实也在做自动驾驶&#xff0c;但是吧&#xff0c;基本都在L2级别。换句话说就是在应急时刻内&#xff0c;还是需要人来辅助驾驶&#xff0c;AI驾驶只是决策层&#xff0c;并不能完全掌握…

【OSG学习笔记】Day 9: 状态集(StateSet)与渲染优化 —— 管理混合、深度测试、雾效等渲染状态

干货开始。_ 一、StateSet核心概念与作用 StateSet 是OSG(OpenSceneGraph)中管理渲染状态的核心类,用于封装 OpenGL 渲染状态(如混合、深度测试、雾效、材质、纹理、着色器等),并将这些状态应用于节点或几何体。 通过合理组织 StateSet,可实现: 渲染状态的高效复用:…

Operating System 实验七 Linux文件系统实验

实验目标: 使用dd命令创建磁盘镜像文件ext2.img并格式化为ext2文件系统,然后通过mount命令挂载到Linux主机文件系统。查看ext2文件系统的超级块的信息,以及数据块的数量、数据块的大小、inode个数、空闲数据块的数量等信息 在文件系统中创建文件xxxxx.txt(其中xxxxx为你的学…

linux中shell脚本的编程使用

linux中shell脚本的编程使用 1.shell的初步理解1.1 怎么理解shell1.2 shell命令 2.shell编程2.1 什么是shell编程2.2 C语言编程 和 shell编程的区别 3.编写和运行第一个shell脚本程序3.1 编写时需要注意以下几点&#xff1a;3.1.1 shell脚本没有main函数&#xff0c;没有头文件…

图像畸变-径向切向畸变实时图像RTSP推流

实验环境 注意&#xff1a;ffmpeg进程stdin写入两张图片的时间间隔不能太长&#xff0c;否则mediamtx会出现对应的推流session超时退出。 实验效果 全部代码 my_util.py #进度条 import os import sys import time import shutil import logging import time from datetime i…