ThreadLocal原理、内存泄漏的验证

news2024/11/25 23:19:29

文章目录

  • 前言
  • 正文
    • 1、ThreadLocal 的常见使用场景
    • 2、从ThreadLocal的源码开始
      • 2.1 ThreadLocalMap
      • 2.2 ThreadLocalMap的 set 方法
      • 2.3 ThreadLocalMap的 remove 方法
      • 2.4 ThreadLocal 的 set 方法
      • 2.5 ThreadLocal 的 remove 方法
    • 3、内存泄漏
      • 3.1 内存泄漏的概念
      • 3.2 为什么说entry的key设计为弱引用,只是规避了部分内存泄漏的情况?
    • 4、ThreadLocal 的正确用法
      • 4.1 及时删除value
      • 4.2 定义ThreadLocal为一个强引用
    • 5、 验证内存泄漏
      • 5.1 验证前的准备
      • 5.2 验证普通情况
      • 5.3 验证内存泄漏情况
      • 5.4 验证处理内存泄漏的情况

前言

Java 在多线程中,想要隔离数据,比如数据库对应的连接对象,在多次请求中,如何保证线程安全,并能保证事务的提交、回滚,我们可以使用 ThreadLocal 这个类。

其原因在于 Thread 类中,定义了属性如下:

public class Thread implements Runnable {
	/** 省略其他代码*/

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

	/** 省略其他代码*/
}

正文

1、ThreadLocal 的常见使用场景

  • 对象跨层传递时,可以避免多次传递,避免层次的约束。因为它本身与线程相关,是线程的一个属性,只要线程不变,就可以通过它传值。
  • 线程间数据隔离。也就是说多线程情况下,数据不会互相影响、覆盖。
  • 事务操作,存储线程事务信息。
  • 数据库连接,Session 等的管理。

本文着重讨论 threadLocals 这个变量。以及这个 ThreadLocal 是如何规避内存泄漏的。

2、从ThreadLocal的源码开始

每一个Thread对象都有这个 threadLocals 变量 ,它存储了当前线程中所有 ThreadLocal 对象,及其对应的值。

2.1 ThreadLocalMap

ThreadLocal 类中,定义了一个静态类 ThreadLocalMap
内容大概如下:

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

           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
           }
	}

 	private Entry[] table;
 	
 	/** 省略其他代码*/
}

可以看到内部定义了一个 Entry 类,并且继承了 “弱引用”。然后定义有一个 Entry 数组,用于存多个ThreadLocal对象和它对应的值。

所谓弱引用,简单来说,就是在JVM垃圾回收时,只要被发现,就会被回收掉。
若是不太了解的同学,可以先看看 Java 中的四种引用:https://blog.csdn.net/FBB360JAVA/article/details/104278183

使用时的大致关系如下:
在这里插入图片描述

当我们手动将线程栈和堆空间实例的强引用去掉时,也就是代码中设置了 threadlocal=null。关系图就发生了变化:
在这里插入图片描述
此时的堆中threadlocal实例可以当GC发生时,会自动回收掉。但是注意,这里被回收掉的只是 entry对象的 key。也就是 ThreadLocal对象本身。至于它的值,并没有被回收。需要同时也回收掉value值,就得调用他那个 remove方法。至于原理,请接着往下看!

2.2 ThreadLocalMap的 set 方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

	// 拿到当前的 entry 数组
    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)]) {
         // 获取当前索引对应的 ThreadLocal对象
        ThreadLocal<?> k = e.get();

		// key值相同时,只修改 value
        if (k == key) {
            e.value = value;
            return;
        }
		// 槽位是过期key,替换占用过期槽位
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 索引i处槽位空,构建新Entry放进槽位,最后检查是否需要扩容
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

2.3 ThreadLocalMap的 remove 方法

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) {
        	// 清除key,即ThreadLocal对象
            e.clear();
            // 清除entry中的value、以及entry对象本身,其实就是赋值为null
            expungeStaleEntry(i);
            return;
        }
    }
}

2.4 ThreadLocal 的 set 方法

其实就是调用了 ThreadLocalMapset 方法。具体内容如下:

public void set(T value) {
	// 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 threadLocals 变量,类型为 ThreadLocalMap 
    ThreadLocalMap map = getMap(t);
    // ThreadLocalMap 不为空时,调用其set方法,存储 ThreadLocal对象以及它对应的值
    if (map != null) {
        map.set(this, value);
    } else {
    	// ThreadLocalMap 为空时,表示第一次设置值,会 new 一个ThreadLocalMap ,并且存储 ThreadLocal对象以及它对应的值
        createMap(t, value);
    }
}

2.5 ThreadLocal 的 remove 方法

其实就是调用了 ThreadLocalMapremove 方法。具体内容如下:

public void remove() {
	// 获取当前线程的 threadLocals 变量,类型为 ThreadLocalMap 
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}

3、内存泄漏

3.1 内存泄漏的概念

程序在申请内存以后,不能释放已经申请的内存空间。在Java中的体现就是,不会再被使用到的对象或变量,他们一直占用着内存。在多次泄漏后会报出OOM

在这里插入图片描述
在我们的线程栈的 ThreadLocal引用 和 堆中的ThreadLocal对象之间,没有了强引用之后,只要发生GCThreadLocal对象就会被回收,与此同时 Entry中的key也会被置为null。如果在这时, Entry 中的 value 还保持着强引用,那就只能等待当前线程执行结束,当前线程的引用和当前线程对象被回收时,它才能被回收。也就是说当前线程如果迟迟不结束(比如线程池的线程复用),那么这个变量value就不会被回收,我们称这种情况为ThreadLocal的内存泄漏。

3.2 为什么说entry的key设计为弱引用,只是规避了部分内存泄漏的情况?

当 key 为强引用时,ThreadLocalMap 就拥有了 ThreadLocal 的强引用,即便我们切断了线程栈方面的强引用,这个entry 中的key也是回收不掉的。除非手动设置删除。那么这种情况就会导致 Entry 的内存泄漏。

当 key 为弱引用时,ThreadLocalMap 就拥有了 ThreadLocal 的弱引用。当我们切断了线程栈方面的强引用,这个 entry中的key会在下次GC时被回收。此时,key就是 null,在我们下一次调用ThreadLocalMap 的 get、set、remove方法时,会自动删除 value,就安全了。一般我们建议使用remove方法删除value。

4、ThreadLocal 的正确用法

4.1 及时删除value

每次使用完ThreadLocal 都调用它的remove方法清除数据。

4.2 定义ThreadLocal为一个强引用

类似于GC ROOT 一样的存在。
将ThreadLocal定义为 private final static,这样就能保证身为弱引用的key会一直在(可以通过ThreadLocal的弱引用访问Entry的value值,随后清除掉)。

5、 验证内存泄漏

5.1 验证前的准备

我当前的Java是11版本,没有自带的 Visual VM 工具,需要自行安装。
安装插件:Visual VM
下载地址

太慢了的话,可以使用百度云下载 提取码:qhjs

IDEA中再安装插件 VisualVM Launcher
在这里插入图片描述
再配置你的运行:
在这里插入图片描述
配置你自己的exe文件
在这里插入图片描述
然后启动你的项目,Visual VM就会自动弹出来。
随后安装 Visual中的插件 Visual GC:
点击弹出来的界面中的 Tools -> Plugins
选择 Visual GC,并点击 Install。
在这里插入图片描述

至此准备工作就完毕了。

5.2 验证普通情况

只 new 对象,并且不存放到 ThreadLocal中。
预期结果是,当该对象没有强引用时,能够回收。
验证代码如下:

package com.example.threadlocal;

import lombok.NonNull;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ThreadLocal 验证内存泄漏
 *
 * @version V1.0
 * @author: fengjinsong
 * @date: 2023年02月08日 11时22分
 */
public class ThreadLocalDemo {

    /**
     * 定义线程池:核心线程数、最大线程数都是5
     */
    static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5,
            1L, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(),
            new ThreadFactory() {
                final AtomicInteger atomicInteger = new AtomicInteger(1);

                @Override
                public Thread newThread(@NonNull Runnable runnable) {
                    return new Thread(runnable, "custom" + atomicInteger.getAndAdd(1));
                }
            });


    public static void main(String[] args) {
        System.out.println("-Xms:" + Runtime.getRuntime().totalMemory() / 1024 / 1024);
        System.out.println("-Xmx:" + Runtime.getRuntime().maxMemory() / 1024 / 1024);


        int count = threadPoolExecutor.prestartAllCoreThreads();
        System.out.println("当前线程池启动的线程数:" + count);

        for (int i = 0; i < 500; i++) {
            // 执行任务
            threadPoolExecutor.execute(() -> {
                // 测试场景1:只new对象,不会内存泄漏
                new Demo();

                // 测试场景2:只set对象到ThreadLocal实例中,会出现内存泄漏,空间回收不掉的情况
                // ThreadLocal<Demo> threadLocal = new ThreadLocal<>();
                // threadLocal.set(new Demo());

                // 测试场景3:set并remove,不会内存泄漏
                // ThreadLocal<Demo> threadLocal = new ThreadLocal<>();
                // threadLocal.set(new Demo());
                // threadLocal.remove();

                System.out.println("执行 " + Thread.currentThread().getName());
            });

            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    public static class Demo {
        // 5m大小
        public byte[] text = new byte[1024 * 1024 * 5];
    }
}

以上代码只是创建了一个匿名对象,当线程不结束,但是发生了垃圾回收,会回收空间。
监控现象如下:
未手动进行垃圾回收(没点击 Perform GC时)
在这里插入图片描述
手动进行垃圾回收(点击Perform GC时),发现空间是回收了的。
在这里插入图片描述

5.3 验证内存泄漏情况

这种情况需要是给 ThreadLocal 中 set ,但是不进行其他操作(不进行remove)。会发生内存泄漏。

// 测试场景2:只set对象到ThreadLocal实例中,会出现内存泄漏,空间回收不掉的情况
ThreadLocal<Demo> threadLocal = new ThreadLocal<>();
threadLocal.set(new Demo());

仍然使用 5.2小节 中的代码,注释掉场景1的代码,放开场景2的代码。
监控现象如下:
未手动进行垃圾回收(没点击 Perform GC时)
在这里插入图片描述
手动进行垃圾回收(点击Perform GC时),发现空间回收不掉。
在这里插入图片描述
只set并进行垃圾回收时,回收不掉空间。点击抽样器,查看内存。发现byte[]占用最高。
在这里插入图片描述
在这里插入图片描述
当前有很多个Demo没有回收掉。GC中能看到老年代回收不了多少东西。
在这里插入图片描述

5.4 验证处理内存泄漏的情况

使用 set 后,最终 remove。可以有效避免内存泄漏。
代码验证仍然使用 5.2小节的代码。打开场景3的注释,同时注释掉其他场景。

// 测试场景3:set并remove,不会内存泄漏
ThreadLocal<Demo> threadLocal = new ThreadLocal<>();
threadLocal.set(new Demo());
threadLocal.remove();

观察到的情况是:
未手动进行垃圾回收(没点击 Perform GC时)

在这里插入图片描述
手动进行垃圾回收(点击Perform GC时),发现空间是回收了的。
在这里插入图片描述

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

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

相关文章

《唐诗三百首》数据源网络下载

2023年的 元宵之夜&#xff0c;这场以“长安”为主题的音乐会火了&#xff01;在抖音&#xff0c;超过2300万人次观看了直播&#xff0c;在线同赏唐诗与交响乐的融合。许多网友惊呼&#xff0c;上学时那些害怕背诵的诗句&#xff0c;原来还可以有这么美的表达这场近80分钟的音乐…

【dapr】服务调用(Service Invokation) - app id的解析

逻辑图解 上图来自Dapr官网教程&#xff0c;其中Checkout是一个服务&#xff0c;负责生成订单号&#xff0c; Order Processor是另一个服务&#xff0c;负责处理订单。Checkout服务需要调用Order Processor的API&#xff0c; 让Order Processor获取到其生成的订单号并进行处理。…

Chapter4:机器人仿真

ROS1{\rm ROS1}ROS1的基础及应用&#xff0c;基于古月的课&#xff0c;各位可以去看&#xff0c;基于hawkbot{\rm hawkbot}hawkbot机器人进行实际操作。 ROS{\rm ROS}ROS版本&#xff1a;ROS1{\rm ROS1}ROS1的Melodic{\rm Melodic}Melodic&#xff1b;实际机器人&#xff1a;Ha…

打造安全可信的通信服务,阿里云云通信发布《短信服务安全白皮书》

随着数字化经济的发展&#xff0c;信息保护和数据安全成为企业、个人关注的焦点。近日&#xff0c;阿里云云通信发布《短信服务安全白皮书》&#xff0c;该白皮书包含安全责任共担、安全合规、安全架构三大板块&#xff0c;呈现了阿里云云通信在信息安全保护方面的技术能力、安…

【知识梳理】Go语言核心编程

基础知识 Go语言就是为了解决编程语言对并发支持不友好、编译速度慢、编程复杂这三个问题而诞生的 特点: Go语言选择组合思想,抛弃继承关系通过接口组合,自由组合成新接口,用接口实现层与层之间的解耦语言特性对比: package mainimport "fmt"func main() {fmt…

防灾必看,边滑坡安全预警解决方案

一、行业背景在我国大部分地区经常会有雨季发生&#xff0c;大量的雨水渗透到了土壤内部&#xff0c;长时间饱含雨水的土壤会变得很重而且还会减少与下方岩石之间的摩擦力&#xff0c;顺着山坡这个滑梯滑下去&#xff0c;造成崩塌、滑坡、泥石流等地质灾害。地质灾害每年都是有…

ccs导入工程失败的处理方法

文章目录当导入CCS新工程时出现下述错误怎么办&#xff1f;方法一 从TI官网下载安装包进行安装&#xff0c;下载链接&#xff1a;软件下载完成 安装路径为上面的文件夹点击安装完成后&#xff0c;导入安装路径&#xff0c;并点击Refresh按钮&#xff0c;依据路径进行更新&#…

电磁隔离放大器-模拟信号4-20mA/0-±10mA/0-±10V转0-75mV/0-2.5V/0-20mA信号转换模块

概述&#xff1a;导轨安装DIN11 IPO EM系列模拟信号隔离放大器是一种将输入信号隔离放大、转换成按比例输出的直流信号混合集成电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等需要电量隔离测控的行业。该模块内部嵌入了一个高效微功率的电源&#xff0c;…

10条终身受益的Salesforce职业发展建议!

Salesforce这个千亿美金巨兽&#xff0c;在全球范围内有42,000多名员工。作为一家发展迅速的科技公司&#xff0c;一直在招聘各种角色&#xff0c;包括销售、营销、工程师和管理人员等。 据IDC估计&#xff0c;从2016年到2020年&#xff0c;该生态系统创造了190万个工作岗位。…

解读 Java 云原生实践中的内存问题

作者&#xff1a;风敬&#xff08;谢文欣&#xff09; Java 凭借着自身活跃的开源社区和完善的生态优势&#xff0c;在过去的二十几年一直是最受欢迎的编程语言之一。步入云原生时代&#xff0c;蓬勃发展的云原生技术释放云计算红利&#xff0c;推动业务进行云原生化改造&…

电子科技大学人工智能期末复习笔记(四):概率与贝叶斯网络

目录 前言 概率 概率公式 贝叶斯公式 链式条件概率 例题 1. 求联合概率分布/边缘概率分布/条件概率分布 2. 灵活运用贝叶斯公式 概率总结 贝叶斯网络 判断独立性 两个事件独立的判断 条件独立性的判断 假设条件独立的链式法则 ⚠Active / Inactive Paths 判断独…

每个前端都应该掌握的7个代码优化的小技巧

本文将介绍7种JavaScript的优化技巧&#xff0c;这些技巧可以帮助你更好的写出简洁优雅的代码。 1. 字符串的自动匹配&#xff08;Array.includes&#xff09; 在写代码时我们经常会遇到这样的需求&#xff0c;我们需要检查某个字符串是否是符合我们的规定的字符串之一。最常…

MySQL进阶篇之锁(lock)

05、锁 5.1、概述 1、介绍 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、RAM、I/O&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据…

[DiceCTF 2023] rRabin

一点点学习别人的WP&#xff0c;这回看到一个大姥(r3kapig)的帖子&#xff0c;DiceCTF第二名&#xff0c;不过有好多东西一时还理解不了&#xff0c;得慢慢来。题目这个题有3个功能&#xff1a;rsa加密功能&#xff0c;p,q,N未知&#xff0c;e17低加密指数解密&#xff0c;不过…

JAVA的垃圾收集器与内存分配策略【一篇文章直接看懂】

内存动态分配和垃圾收集技术是JAVA和C之间最大的区别之一 垃圾收集&#xff08;Garbage Collection&#xff0c;GC&#xff09;只办三件事&#xff1a; 哪些内存需要回收什么时候回收如何回收 对于对象回收的方法 引用计数法&#xff1a; 每处引用时1&#xff0c;引用失效…

软件测试标准流程

软件测试的基本流程大概要经历四个阶段&#xff0c;分别是制定测试计划、测试需求分析、测试用例设计与编写以及测试用例评审。因此软件测试的工作内容&#xff0c;远远没有许多人想象的只是找出bug那么简单。准确的说&#xff0c;从一个项目立项以后&#xff0c;软件测试从业者…

第一章 认识Python

本章目录 一、初识Python 二、Python环境安装 三、Python代码的执行 四、Python集成开发环境 五、Python2.x与Python3.x的区别 六、本章小结 Python代码的编辑和运行方式主要分为两种&#xff1a;交互模式和脚本模式。 在交互模式下&#xff0c; 用户输入Python代码并按…

非常棒的13款3DMax渲染器插件推荐给大家

3Ds Max 可能是具有最多可与其集成的外部渲染引擎的 3D 软件包。 今天我们将看看 13 个最好的 3Ds max 渲染插件&#xff0c;我们将从以下列表开始&#xff1a; 13- Radeon ProRender ProRender 的正式名称为 FireRender&#xff0c;是 AMD 的开源路径追踪器。这个 3ds Max …

Redis的缓存雪崩、击穿、穿透和解决方案

2.5 缓存穿透问题的解决思路 缓存穿透 &#xff1a;缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效&#xff0c;这些请求都会打到数据库。 常见的解决方案有两种&#xff1a; 缓存空对象 优点&#xff1a;实现简单&#xff0c;维护…

程序员不得不知道的 API 接口常识

说实话&#xff0c;我非常希望自己能早点看到本篇文章&#xff0c;大学那个时候懵懵懂懂&#xff0c;跟着网上的免费教程做了一个购物商城就屁颠屁颠往简历上写。 至今我仍清晰地记得&#xff0c;那个电商教程是怎么定义接口的&#xff1a; 管它是增加、修改、删除、带参查询…