ThreadLocal原理及内存泄漏模拟

本文探讨了Java中的ThreadLocal原理,其作为线程局部存储,通过ThreadLocalMap实现在每个线程内的独立变量。不当使用可能导致内存泄漏,因为线程未结束时,Entry对象无法被回收。解决方案是在不再需要ThreadLocal时调用remove方法。文章还通过模拟实验展示了内存泄漏的现象,解释了ThreadLocalMap中referent对象在垃圾回收后变为null的情况,揭示了内存泄漏的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal原理

  • ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,成为线程内的局部变量
  • 该线程可以在任意时刻、任意⽅法中获取缓存的数据
  • 这样每个线程都自己管理自己的局部变量,别的线程操作的数据不会产生影响,互不影响
  • 一般使用ThreadLocal,官方建议定义为private static
  • ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对 象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的 值
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

在这里插入图片描述
在这里插入图片描述

  • 其中2为弱引用

ThreadLocal内存泄漏问题

  • 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
  • 不当使用ThreadLocal可能会导致内存泄漏
  • 因为当ThreadLocal对象使⽤完之后,应该要 把设置的key,value,也就是Entry对象进⾏回收
  • 如果线程长期没有结束(如线程池中的核心线程),线程对象是通过 强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收, Entry对象也就不会被回收,从⽽出现内存泄漏
  • 解决办法:在使⽤了ThreadLocal对象之后,手动调⽤ThreadLocal的remove⽅法,手动清除Entry对象

ThreadLocal内存泄漏模拟

过程剖析

  • 在网上查阅了大量的文章,发现将ThreadLocal原理的文章一抓一大把,而且内容千篇一律,但却鲜有将内存泄漏现象模拟出来的。
  • 因此,本文的重点是对ThreadLocal内存泄漏现象的模拟,但原理是模拟的前提,因此还是提前介绍了原理。
  • 定义一个类继承Thread,并重写run方法,run方法里编写模拟逻辑:为threadlocal对象赋值 —》将threadlocal对象置空—》调用System.gc()手动进行垃圾回收—》观察前后 Thread对象中 ThreadLocalMap的变化

public class TestForThreadLocal {

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

    public static void main(String[] args) {

        TestThread test = new TestThread();
        test.start();
    }
}

class TestThread extends Thread {

    @Override
    public void run() {
        System.out.println("start...");
        TestForThreadLocal test = new TestForThreadLocal();
        test.threadLocal.set("test");
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test = null;
        System.gc();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end....");
    }
}
  1. 在执行 test = null前
    在这里插入图片描述
  • 可以发现,线程的threadLocals的table中,10号引用指向了 referent为ThreadLocal@492的对象,value为“test”
  1. 调用System.gc()进行垃圾回收后
    在这里插入图片描述
  • 可以发现,此时10号的referent已经变为null,但value没有变化,仍然为“test”
  • 这就已经出现了内存泄漏现象:value = “test”这个值一直存在,但却没有引用能够访问到它了。

深入源码

  • 那么这个referent对象是啥,为啥会因为gc而变为null?
/**
	可以看到,map对象 entry继承了弱引用类
	在构造方法中,调用了super(k)将 key对象传给了父类
**/
public class ThreadLocal<T> {
	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {
	            /** The value associated with this ThreadLocal. */
	            Object value;
	
	            Entry(ThreadLocal<?> k, Object v) {
	                super(k);
	                value = v;
	            }
        }
	}
}

/**
	在其父类中,我们找到了想要找的 referent
**/
public class WeakReference<T> extends Reference<T> {

	public WeakReference(T referent) {
	       super(referent);
	   }
}
/**
	通过将referent加入 弱引用队列,保证下次gc时,referent能够被回收,因此便出现了 gc后 该值变为null的情况
**/
public abstract class Reference<T> {
    Reference(T referent) {
        this(referent, null);
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值