ThreadLocal内存泄漏与解决

news/2024/11/23 5:37:57/

目录

什么是Threadlocal?

Threadlocal的基本使用

ThreadLocal的内存泄漏举例

场景1

场景2

场景3

场景4

内存泄漏原因分析

总结


什么是Threadlocal?

   ThreadLocal 是 Java 中的一个类,它提供了线程本地变量的支持。线程本地变量是指被线程拥有并独立于其他线程的变量。每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。ThreadLocal 主要用于在多线程环境下保持变量的线程封闭性,以实现线程安全。

Threadlocal的基本使用

1. set(T value)

用于设置当前线程的线程本地变量的值。

参数 value 是要设置的值。

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, ThreadLocal!");

 2. get()

用于获取当前线程的线程本地变量的值。

ThreadLocal<String> threadLocal = new ThreadLocal<>();
String value = threadLocal.get();

3. remove()

用于移除当前线程的线程本地变量。

在一些情况下,手动调用 remove() 可以帮助避免内存泄漏。

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

 4. initialValue()

该方法是一个 protected 方法,可以被子类重写以提供线程本地变量的初始值。默认情况下,initialValue() 返回 null。

通常情况下,我们会通过 ThreadLocal 的子类并重写 initialValue() 方法来设定线程本地变量的初始值。

public class MyThreadLocal extends ThreadLocal<String> {@Overrideprotected String initialValue() {return "Default Value";}
}// 使用自定义的 MyThreadLocal
MyThreadLocal myThreadLocal = new MyThreadLocal();
String value = myThreadLocal.get();  // 返回 "Default Value"

 上述方法综合使用如下:

public class ThreadLocalExample {// 创建一个 ThreadLocal 实例private static final ThreadLocal<String> threadLocalValue = new ThreadLocal<>();public static void main(String[] args) {// 在主线程设置值threadLocalValue.set("Main Thread Value");// 创建两个子线程Thread thread1 = new Thread(() -> {// 在子线程1获取值String value = threadLocalValue.get();System.out.println("Thread 1: " + value); // 输出:Thread 1: null});Thread thread2 = new Thread(() -> {// 在子线程2设置值threadLocalValue.set("Thread 2 Value");// 在子线程2获取值String value = threadLocalValue.get();System.out.println("Thread 2: " + value); // 输出:Thread 2: Thread 2 Value});// 启动子线程thread1.start();thread2.start();// 在主线程获取值String mainThreadValue = threadLocalValue.get();System.out.println("Main Thread: " + mainThreadValue); // 输出:Main Thread: Main Thread Value// 清理主线程的值threadLocalValue.remove();}
}

       在上述案例中可以看出,每个线程中的Threadlocal变量都是独立的副本。ThreadLocal 提供了一种在多线程环境下安全地存储和访问线程本地变量的机制。每个线程都可以独立地对其进行操作,互不干扰。


ThreadLocal的内存泄漏举例

场景1

任务中不执行任何有意义的代码

//  将堆内存大小设置为-Xmx256m
public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {
//                    LocalVariable localVariable = new LocalVariable();
//                    ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
//                    oom.threadLocalLV = new ThreadLocal<>();
//                    oom.threadLocalLV.set(new LocalVariable());
//                    oom.threadLocalLV.remove();System.out.println("use local varaible");}});}System.out.println("pool execute over");}}

执行后,我们通过cmd输入jvisualvm启动java性能监控工具(jdk自带),查看当前的性能消耗情况。

查看内存会发现内存消耗稳定在25MB左右。

场景2

在每个任务中new出一个数组,执行完成后我们可以看见,内存占用基本和场景1同

public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {LocalVariable localVariable = new LocalVariable();
//                    ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();
//                    oom.threadLocalLV = new ThreadLocal<>();
//                    oom.threadLocalLV.set(new LocalVariable());
//                    oom.threadLocalLV.remove();System.out.println("use local varaible");}});}System.out.println("pool execute over");}}

启动后内存使用情况如下

 我们可以看到有毛刺现象,这是因为GC造成的,但是没有出现内存泄漏的情况。

场景3

启用Threadlocal

public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {
//                    LocalVariable localVariable = new LocalVariable();ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();oom.threadLocalLV = new ThreadLocal<>();oom.threadLocalLV.set(new LocalVariable());
//                    oom.threadLocalLV.remove();System.out.println("use local varaible");}});Thread.sleep(100);}System.out.println("pool execute over");}}

启动后内存使用情况如下

占用内存情况在100MB-125MB左右。

场景4

手动调用remove清除threadlocal,执行查看内存情况。

public class ThreadLocalMemoryLeak {private static final int TASK_LOOP_SIZE = 500;/*线程池*/final static ThreadPoolExecutor poolExecutor= new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES,new LinkedBlockingQueue<>());static class LocalVariable {private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/}ThreadLocal<LocalVariable> threadLocalLV;public static void main(String[] args) throws InterruptedException {for (int i = 0; i < TASK_LOOP_SIZE; ++i) {poolExecutor.execute(new Runnable() {public void run() {
//                    LocalVariable localVariable = new LocalVariable();ThreadLocalMemoryLeak oom = new ThreadLocalMemoryLeak();oom.threadLocalLV = new ThreadLocal<>();oom.threadLocalLV.set(new LocalVariable());oom.threadLocalLV.remove();System.out.println("use local varaible");}});Thread.sleep(100);}System.out.println("pool execute over");}}

启动后内存使用情况如下

此时内存使用情况跟场景1相同。 

总结:通过以上场景3,我们可以得出在启用了threadlocal以后发生了内存泄漏。


内存泄漏原因分析

        每一个Thread线程都维护了一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal 实例本身,value 是真正需 要存储的Object,也就是说ThreadLocal本身并不存储值,它只是作为一个 key来让线程从ThreadLocalMap获取value。ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC 时会被回收。

        图中虚线表示弱引用。

        当把 threadlocal 变量置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前 线程再迟迟不结束的话,这些key为null的Entry的 value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。

       只有当前线程结束后,线程就不会在栈中,强引用断开,才会被GC回收掉。解决内存泄漏做法是当不使用线程中threadlocal变量时及时remove清除数据。

       场景3中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用 localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。当我们手动再每次都调用remove清除数据时,内存正常。

      ThreadLocal的实现中,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。 只有 remove()方法中显式调用了expungeStaleEntry 方法。

总结

       JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。

       JVM 利用调用remove、get、set方法的时候,回收弱引用。

       当ThreadLocal存储很多Key为 null的Entry的时候,而不再去调用 remove、 get、set 方法,那么将导致内存泄漏。

        使用线程池+ ThreadLocal 时要多注意,这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。


http://www.ppmy.cn/news/1296468.html

相关文章

进程间通信之匿名管道和命名管道的理解和实现【Linux】

进程间通信之匿名管道和命名管道的理解和实现 进程间通信什么是管道匿名管道代码实现管道的读写规则管道特点 命名管道创建命名管道代码实现 进程间通信 进程间通信的目的 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程资源共享&#xff1a;多个进程之间共享同…

chromium通信系统-ipcz系统(十)-chromium通信系统-ipcz系统(十一)-mojo binding

关于mojo binding的官方文档为mojo docs。 由于比较复杂&#xff0c;这里只做简单源码分析。 我们知道要实现rpc&#xff0c;必须实现客户端和服务端。 mojo 实现了一套领域语言&#xff0c;通过领域语言描述接口和数据&#xff0c; 再通过特有编译器编译成c代码。 这个过程会…

外汇网站主要业务逻辑梳理

上图为工行ICBC的外汇保证金交易界面。 当需要买入帐户欧元&#xff08;欧元人民币&#xff09;时&#xff0c;买入100欧元&#xff0c;因为没有杠杆&#xff0c;虽然欧元中间价是782.34&#xff0c;但实际需要支付783.14元人民币的保证金&#xff0c;这个兑换不是真实的外汇兑…

游戏美术的技与艺

大家好&#xff0c;我是阿赵。   可能很多朋友都知道&#xff0c;我刚进入游戏行业的时候&#xff0c;做的是美术工作&#xff0c;包括了建模、贴图、动画等&#xff0c;都做过。我对各种美术资源制作也都很熟悉&#xff0c;懂得很多制作的技术。但最后&#xff0c;我却没有继…

2024--Django平台开发-基础信息(一)

一、前置知识点 - Python环境搭建 (Python解释器、Pycharm、环境变量等) - 基础语法(条件、循环、输入输出、编码等) - 数据类型(整型、布尔型、字符串、列表、字典、元组、集合等) - 函数(文件操作、返回值、参数、作用域等) - 面向对象 (类、对象、封装、继承、多态等)包和模…

Java多线程技术11——ThreadPoolExecutor类的使用1

1 概述 ThreadPoolExecutor类可以非常方便的创建线程池对象&#xff0c;而不需要程序员设计大量的new实例化Thread相关的代码。 2 队列LinkedBlockingQueue的使用 public class Test1 {public static void main(String[] args) {LinkedBlockingQueue queue new LinkedBlocki…

Rust 切片与Go 有何区别?

大家好&#xff0c;我是渔夫。 今天&#xff0c;让我们一起深入学习 Rust 中的切片。在 Rust 编程语言中&#xff0c;除了常见的 String 类型&#xff0c;还有一种重要的字符串类型&#xff1a;str。这种类型通常被称为字符串切片&#xff08;slice&#xff09;。 字符串切片…

【QML COOK】- 002-添加一个图片

1. 编辑main.qml import QtQuickWindow {width: 800height: 800visible: truetitle: qsTr("Hello World")Image {anchors.fill: parentsource: "qrc:/Resources/Images/arrow.png"} }将Window的width和height都改成800&#xff0c;因为我们要添加的图片大…