详解ThreadLocal

news2024/11/13 11:14:56

为什么出现ThreadLocal ?

在多线程环境下,如果多个线程同时修改一个公共变量,可能会出现线程安全问题,即该变量的最终结果可能出现异常。为了解决线程安全问题,JDK提供了很多技术手段,比如使用synchronized或Lock来给访问公共资源的代码上锁,保证了代码的原子性。但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

ThreadLocal是什么?

ThreadLocal是一个与线程相关的类,但它本身并不是一个Thread。

这个类可以提供线程局部变量,与普通变量有所不同。虽然你可以实例化一个ThreadLocal对象,但当每个线程访问或设置它时,它们实际上是在操作本线程内的该对象的副本。这也意味着,这个对象在不同的线程中,副本的值是不一样的

ThreadLocal是Java提供的一个线程本地变量的实现机制,其核心目的是让每个线程拥有自己独立的变量副本。这样,在多线程环境下,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本,从而实现了线程数据隔离,避免了线程安全问题。

如何使用?

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocalVar = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程1中的本地变量值
                threadLocalVar.set("Hello");
                // 打印本地变量
                System.out.println("Thread 1 value: " + threadLocalVar.get());
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 设置线程2中的本地变量值
                threadLocalVar.set("World");
                // 打印本地变量
                System.out.println("Thread 2 value: " + threadLocalVar.get());
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

我们创建了两个线程(t1和t2),每个线程都设置了不同的本地变量值,并打印出来。通过使用ThreadLocal类,每个线程都有自己独立的本地变量副本,因此它们之间不会相互干扰。

输出结果

Thread 1 value: Hello
Thread 2 value: World

 注意:

  • ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
  • ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法

 ThreadLocal如何实现?

public class ThreadLocal<T> {
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 根据当前ThreadLocal对象获取对应的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 返回Entry对象的值作为结果
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果map为空或者Entry对象为空,则设置初始值并返回
        return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
        // 返回当前线程的threadLocals成员变量
        return t.threadLocals;
    }
    
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 将给定的ThreadLocal对象和对应的值存储到table中
            map.set(this, value);
        } else {
            // 如果map为空,则创建一个新的ThreadLocalMap对象并存储给定的值
            createMap(t, value);
        }
    }
    ThreadLocalMap getMap(Thread t) {
        // 返回当前线程的threadLocals成员变量
        return t.threadLocals;
    }
}

public class Thread implements Runnable {
    //...
    // 定义一个ThreadLocalMap类型的成员变量threadLocals,用于存储当前线程的ThreadLocal对象及其对应的值
    ThreadLocal.ThreadLocalMap threadLocals = null;   
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 
}

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;  // 存储键值对的Entry数组
        static class Entry extends WeakReference<ThreadLocal<?>> {
          Object value;
          Entry(ThreadLocal<?> k, Object v) {
              super(k);  // 使用super()方法创建弱引用,key值为ThreadLocal对象
              value = v;
          }
        }
        //...
        private Entry getEntry(ThreadLocal<?> key) {
          int i = key.threadLocalHashCode & (table.length - 1);  // 计算索引位置
          Entry e = table[i];
          if (e != null && e.get() == key)
              return e;
          else
              return getEntryAfterMiss(key, i, e);  // 如果未找到,则进行查找操作
        }
        private void set(ThreadLocal<?> key, Object value) {
            // 将给定的ThreadLocal对象和对应的值存储到table中
        }
         private void remove(ThreadLocal<?> key) {
      Entry[] tab = table;
      int len = tab.length;
      int i = key.threadLocalHashCode & (len-1);
      for (Entry e = tab[i];
            e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
               e.clear();
               expungeStaleEntry(i);
               return;
            }
          }
        }
    }
}

ThreadLocalMap 本质是一个<key,value>形式的节点组成的数组,

ThreadLocalMap 的节点是一个静态内部类 Entry,它继承自 WeakReference<ThreadLocal<?>>。这意味着每个 Entry 对象都是一个弱引用,其引用的是 ThreadLocal 类型的对象作为键(key)。值为代码放入的值

      弱引用

  • 不阻断垃圾回收:与强引用不同,弱引用不会增加对象的引用计数,因此即使有弱引用指向一个对象,该对象也可能在任何时间被垃圾回收器回收。
  • 生命周期短暂:一旦垃圾回收器发现某个对象只被弱引用指向,不管当前内存是否足够,这个对象都会被回收。

图源 https://javabetter.cn/sidebar/sanfene/javase.html 

看张经典的图帮助我们理解

 

图源来自阿里文档 

ThreadLocal的实现原理可以分为两个关键部分:

  1. 每个线程(Thread)内部都有一个独立的Map对象,用于存储数据。由于每个线程都有自己的Map,因此不同线程之间的数据是隔离的,不会互相干扰。

  2. 当我们创建一个ThreadLocal对象时,这个对象将作为键(key)来帮助我们在线程内部的Map中定位数据。

下面是对ThreadLocal的核心机制的简要概括:

  • 我们创建的ThreadLocal对象仅仅是一个键(key)。
  • 每个线程(Thread)内部都有一个Map,这个Map中存储了ThreadLocal对象(作为键)和该线程为这个ThreadLocal对象设置的值(作为值)。
  • 对于不同的线程来说,每次获取自己的副本值时,其他线程无法访问到当前线程的副本值,从而实现了副本之间的隔离,互不干扰。

 ThreadLocal的内存泄漏问题

ThreadLocal可能会引起内存泄漏问题

首先,了解内存泄漏的基础知识是必要的。内存泄漏是指一块被分配的内存既不能被访问,也无法被垃圾回收器回收的情况。在Java中,强引用与弱引用对垃圾回收的影响不同,强引用会阻止对象被垃圾回收,而弱引用则不会。

ThreadLocal对象(作为ThreadLocalMap中的键)被垃圾回收器回收,而与之关联的ThreadLocalMap仍然存活时(由于其生命周期通常与线程相同),如果对应的值没有被适当地清理,就会出现以下情况:

  • ThreadLocalMap中仍然存在键(ThreadLocal对象)为null的条目。
  • 这些条目的值(即ThreadLocal对象所关联的数据)依然占用内存。
  • 由于ThreadLocal对象已被回收,这些值无法通过正常的引用路径被访问或清理。

这种情况就会导致内存泄漏问题,因为即使ThreadLocal对象不再需要,其对应的值也无法被垃圾回收,从而占用不必要的内存资源。

为了避免这种内存泄漏,可以采取以下措施:

  1. 显式清理:在使用完ThreadLocal后,调用remove()方法来显式地从ThreadLocalMap中移除对应的键值对。

  2. 合理设计:在设计上,确保ThreadLocal对象的生命周期与使用它的线程相匹配,避免长时间持有不必要的ThreadLocal对象。

  3. 线程池管理:在使用线程池时,注意线程的复用,及时清理线程本地变量,特别是在长生命周期的线程池中。

  4. 资源监控:定期监控和分析应用的内存使用情况,以便及时发现潜在的内存泄漏问题。

那为什么还要设计为弱引用?

在现代程序运行中,线程池模式被广泛采用,这导致线程的生命周期变得相对较长。由于ThreadLocal可以为每个线程提供独立的数据存储,如果这些数据没有得到及时回收,就可能导致内存泄漏和最终的内存溢出(OOM)。

为了减轻程序员手动管理资源的负担,并防止内存溢出的风险,Java引入了一套基于弱引用的自动回收机制。这个机制的核心是使用WeakReference来包装ThreadLocal对象,从而允许垃圾回收器在需要时回收它们。

下面是对这种机制的简要概括:

  • 弱引用包装:ThreadLocal对象被封装在一个弱引用中,这样即使它被用作ThreadLocalMap的键,也不会阻止垃圾回收器回收它。
  • 自动清理:当ThreadLocal对象不再强引用时,垃圾回收器可以自动回收它,从而避免内存泄漏。
  • 资源管理:通过这种方式,程序员不需要显式地调用remove方法来清理ThreadLocal对象,减少了手动管理资源的复杂性。
  • 防止OOM:这种弱引用机制有助于减少长时间运行的线程池中ThreadLocal对象的累积,防止内存溢出。

参考文献:

图解,深入浅出带你理解ThreadLocal-阿里云开发者社区 https://javabetter.cn/sidebar/sanfene/javase.html 

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

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

相关文章

软考高级:软件架构风格-虚拟机风格概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

练习 10 Web [MRCTF2020]你传你呢

和test5一样&#xff0c;文件上传限制为图片&#xff0c;使用.htaccess文件让上传成功图片木马文件进行执行 开屏暴击&#xff0c;差点去世 尝试上传文件 各种过滤&#xff0c;但是能传图片&#xff0c;这里就不写了&#xff0c;我做过的test5遇到过的重复内容 直接尝试在te…

HarmonyOS/OpenHarmony应用开发-DevEco Studio 在MAC上启动报错

报错截图 报错详细内容 ------------------------------------- Translated Report (Full Report Below) -------------------------------------Process: devecostudio [8640] Path: /Applications/DevEco-Studio.app/Contents/MacOS/devecos…

ab (Apache benchmark) - 压力/性能测试工具

Apache benchmark&#xff08;ab&#xff09; 安装window安装使用方法 - bin目录运行使用方法 - 任意目录运行 linux安装 基本命令介绍常用参数:输出结果分析&#xff1a; ab的man手册 安装 window安装 官网下载链接&#xff1a;https://www.apachehaus.com/cgi-bin/download…

docker启动卡死问题排查

问题&#xff1a;输入docker ps 或则vession 卡死&#xff0c;无任何输出 排查思路如下&#xff1a; 1、查看docker状态或者日志 journalctl -u docker.service 或者 systemctl status docker 3月 20 18:23:06 dfbpmyy2 dockerd[1114]: time"2024-03-20T18:23:06.7449…

mysql体系结构及主要文件

目录 1.mysql体系结构 2.数据库与数据库实例 3.物理存储结构​编辑 4.mysql主要文件 4.1数据库配置文件 4.2错误日志 4.3表结构定义文件 4.4慢查询日志 4.4.1慢查询相关参数 4.4.2慢查询参数默认值 4.4.3my.cnf中设置慢查询参数 4.4.4slow_query_log参数 4.4.…

【xr806开发板使用】连接wifi例程实现

##开发环境 win10 WSL ##1、环境配置 参考&#xff1a;https://aijishu.com/a/1060000000287513 首先下载安装wsl 和ubuntu https://docs.microsoft.com/zh-cn/windows/wsl/install &#xff08;1&#xff09;安装repo&#xff1a; 创建repo安装目录&#xff1a; mkdir ~/…

练习4-权重衰减(李沐函数简要解析)

环境:练习1的环境 代码详解 0.导入库 import torch from torch import nn from d2l import torch as d2l1.初始化数据 这里初始化出train_iter test_iter 可以查一下之前的获取Fashion数据集后的数据格式与此对应 n_train, n_test, num_inputs, batch_size 20, 100, 200, …

基于cnn深度学习的yolov5+pyqt+分类+resnet+骨龄检测系统

往期热门博客项目回顾&#xff1a; 计算机视觉项目大集合 改进的yolo目标检测-测距测速 路径规划算法 图像去雨去雾目标检测测距项目 交通标志识别项目 yolo系列-重磅yolov9界面-最新的yolo 姿态识别-3d姿态识别 深度学习小白学习路线 YOLOv5与骨龄识别 YOLOv5&a…

C语言指针与地址基础学习(取地址运算)

C语言指针与地址基础学习&#xff08;取地址运算&#xff09; 取地址运算&#xff1a;&运算符取得变量的地址代码示例一运算符& 取地址运算&#xff1a;&运算符取得变量的地址 代码示例一 #include<stdio.h> int main() {int a;a 6;printf("sizeof(i…

通过nginx配置文件服务器(浏览器访问下载)

配置服务器端文件下载和展示(Nginx) nginx.conf文件中增加配置&#xff0c;然后浏览器里访问ip:port回车即可 server { listen port; server_name 服务端ip; # 指定文件下载目录的路径 location / { # 使用root指令来设置文件的根目录 # Nginx会在该目录下寻找相对于loca…

【MySQL】-锁的使用

1、锁的粒度分类 1、全局锁 一般用于数据库备份&#xff0c;整个库只读 FLUSH TABLES WITH READ LOCK 2、表级锁 细分为&#xff1a; 1&#xff09;意向锁 Intention 事务A对表加行级锁&#xff0c;这行记录就只能读不能写。 事务B申请增加表级锁&#xff0c;如果他申请…

【Springboot3+Mybatis】文件上传阿里云OSS 基础管理系统CRUD

文章目录 一、需求&开发流程二、环境搭建&数据库准备三、部门管理四、员工管理4.1 分页(条件)查询4.2 批量删除员工 五、文件上传5.1 介绍5.2 本地存储5.3 阿里云OSS1. 开通OSS2. 创建存储空间Bucket 5.4 OSS快速入门5.5 OSS上传显示文件 六、配置文件6.1 yml配置6.2 C…

linux 命令笔记:gpustat

1 命令介绍 gpustat是一个基于Python的命令行工具&#xff0c;它提供了一种快速、简洁的方式来查看GPU的状态和使用情况它是nvidia-smi工具的一个封装&#xff0c;旨在以更友好和易于阅读的格式显示GPU信息。gpustat不仅显示基本的GPU状态&#xff08;如温度、GPU利用率和内存…

Oracle19C静默安装教程

文章目录 一、安装前的准备1、安装Linux操作系统2、配置网络源或者本地源3、hosts文件配置 二、准备安装环境1、安装依赖包2、创建oracle用户组3、配置系统内核参数4、关闭selinux5、配置oracle用户环境6、修改用户的Shell限制 三、静默安装Oracle数据库1、创建oracle安装目录2…

Oracle19C图形界面安装教程

文章目录 一、安装前的准备1、安装Linux操作系统2、配置网络源或者本地源3、hosts文件配置 二、Oracle19c安装过程1、安装相关软件&#xff1a;2、用户与组&#xff1a;3、修改内核参数&#xff1a;4、资源限制&#xff1a;5、配置用户环境变量&#xff1a;6、创建相关文件目录…

[pytorch] detr源码浅析

[pytorch] detr源码浅析 1. backbone部分2. encoder部分3. decoder部分4. 输出预测 为之后SAM的代码分析做铺垫 1. backbone部分 detr.py中的DETR class class DETR(nn.Module):def __init__(self, backbone, transformer, num_classes, num_queries, aux_lossFalse):...def …

21个 JVM 技术点详解(附面试解答)

最近兄弟们面试&#xff0c;都逃不过被 JVM 问题轰炸的命运&#xff0c;为啥面试官喜欢拿 JVM 说事呢&#xff1f;V 哥认为&#xff0c;除了要问倒你&#xff0c;就是要压你薪水&#xff0c;咱绝对不能怂&#xff0c;俗话说的好&#xff1a;兵来将挡&#xff0c;水来土掩&#…

串行通信协议 SPI

SPI&#xff08;Serial Peripheral Interface&#xff09;是一种串行通信协议&#xff0c;常用于连接微控制器、存储器、传感器和其他外围设备。SPI通常由一个主设备&#xff08;通常是微控制器&#xff09;和一个或多个从设备组成。 1、SPI通信一般由四根线组成: SCLK&#x…

2024 全新测算系统网站源码 二开修复完整版

源码介绍 安装教程 环境&#xff1a;程序为以PHPMYSQL架构&#xff0c;PHP版本5.6&#xff0c;让系统更畅快稳定。适合linux或者windows。 修改数据库/config/inc_config.php后导入数据库 子目录绑定ffsm 后台地址/acs 后台账号admin密码114077 支持功能&#xff1a;微信…