ThreadLocal原理分析+常出现问题【内存泄露、线程复用导致ThreadLocal污染】

🍰 个人主页:不摆烂的小劉
🍞文章有不合理的地方请各位大佬指正。
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️

PageHelper 分页查询【底层代码-图文分析】【原理篇】探讨了PageHelper的底层原理——拦截MyBatis的查询操作,在Sql语句执行前动态地添加分页逻辑,从而实现数据的分页查询。

pagehelper常见问题【分页失效】【ThreadLocal污染线程】探讨了 Pagehelper 使用过程中频繁出现的问题,包括分页失效、多 limit、 异常查询全部数据等情况,解释了出现这些问题的原因,并给出解决措施。

接着这个问题继续研究ThreadLocal

Pagehelper为什么使用ThreadLocal存储分页参数?

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

public static void main(String[] args) {
    threadLocal.set("123456789");
    System.out.println(threadLocal.get());
}

上面代码做了什么?怎么做的?有什么风险问题?怎么解决?

一、前置知识

1.什么是内存溢出和内存泄露,有什么联系?

内存溢出(OOM)
  1. 指程序在运行过程中,申请的内存空间超过了系统所能提供的最大内存空间。
  2. 好比一个容器,已经装满了东西,再也无法容纳更多,一旦继续往里放,就会溢出。
  3. 常出现情景:
    一次性加载大量数据到内存中;
    也可能是因为程序中存在无限循环或递归调用,导致内存不断被占用且无法释放。
内存泄露
  1. 程序在运行过程中,不断申请内存却没有及时释放不再使用的内存空间,导致可用内存逐渐减少。
  2. 这类似于一个人不断向仓库里存放物品,却从不清理不再需要的东西,使得仓库空间越来越小。
  3. 常出现情景:
    忘记关闭文件流、数据库连接等资源,导致这些资源所占用的内存无法被回收;
    在容器类对象中不断添加元素,却没有合理的删除机制

联系:内存泄露是导致内存溢出的一个重要原因。随着内存泄露的不断积累,可用内存越来越少,最终可能引发内存溢出。

2.四种引用类型

1.强引用
  1. new关键字创建的对象所关联的引用就是强引用。
  2. 只要强引用存在,垃圾回收器就不会回收被引用的对象,即使内存空间不足时也不会考虑回收它,而是会直接抛出OutOfMemoryError【内存溢出】错误,直到没有强引用指向该对象时,才会考虑回收它以释放内存
Product product = new Product(); // 强引用product指向堆内存中Product对象
product = null; // 强引用product 被设置为 null 时,不可达,则Product对象被回收
2.软引用
  1. 内存充足时,所指对象不会回收;内存不足时,垃圾回收器会优先回收它以释放空间,避免内存溢出。它适用于实现缓存。
  2. 如果回收了软引用对象之后仍然没有足够的内存,才会抛出OutOfMemoryError【内存溢出】错误。
  3. SoftReference<Object> softRef = new SoftReference<>(new Object());
3.弱引用
  1. 使内存足够,垃圾回收器就可以随时回收该对象。
  2. 弱引用适用于需要临时引用对象的场景,如临时缓存或临时存储对象。
  3. 例,ThreadLocal中的key弱引用
 static class ThreadLocalMap {
   static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
 }
4.虚引用
  1. 虚引用的主要作用不是为了获取对象的引用,而是用于在对象被回收时,接收一个系统通知。
  2. 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收该对象之前,把这个虚引用加入到与之关联的引用队列中,以便程序能得知对象即将被回收的消息。
  3. 如对象资源释放或日志记录,常用于跟踪对象被垃圾回收的状态,执行一些清理工作。

二、ThreadLocal

1.ThreadLocal是什么?解决什么问题?

定义

  • ThreadLocal提供了线程局部变量的功能,允许每个线程都有自己独立的变量副本,这些变量副本对于其他线程是不可见的(隔离性)。
  • 数据结构角度看,ThreadLocal内部维护了一个类似于Map的结构,其中键(Key)是线程对象(Thread),值(Value)是该线程对应的变量副本

解决问题:方便传递参数,用户身份信息存储在ThreadLocal变量中,在这个线程的各个方法中都可以方便地获取

2.ThreadLocal底层如何存储

下面代码是什么?做了什么?怎么做的

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

public static void main(String[] args) {
    threadLocal.set("123456789");
    System.out.println(threadLocal.get());
}
  1. private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

进入ThreadLocal看一下上面代码做了什么?

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 初始化
protected T initialValue() {
      return null;
}

threadLocalHashCode作用

  • 计算ThreadLocal对象在内部数组中的存储位置
  • 区分不同的 ThreadLocal对象,没有这个唯一的哈希码,不同的ThreadLocal对象可能会错误地覆盖
  1. 存储变量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);
}

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
  • Thread.currentThread();先获取当前线程对象
  • getMap(t);获取当前线程ThreadLocalMap类型变量副本
  • 其中ThreadLocalMap中也定义了一个内部类Entry如下
 static class Entry extends WeakReference<ThreadLocal<?>> {
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
          super(k);
          value = v;
      }
  }

Entry中的keyThreadLocalvalue为咱们要存储的值

所以根据上述代码,线程第一次执行threadLocal.set

  • 获取当前线程变量副本

  • 创建一个Entry的数

  • 计算存储位置,并存储


 private void set(ThreadLocal<?> key, Object value) {
  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<?> k = e.get();
        if (k == key) {
            e.value = value;
            return;
        }
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

再次执行set方法时

  • 计算存储位置,如果有冲突,使用线性探测的方式循环寻找新的插入位置
  • 插入元素
  • 调整容器大小

3.获取变量threadLocal.get()

public T get() {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             T result = (T)e.value;
             return result;
         }
     }
     return setInitialValue();
 }

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);
 }
  • 获取当前线程对象
  • 获取当前线程的变量副本ThreadLocalMap
  • 调用map.getEntry方法,根据threadLocalHashCode获取元素

三、ThreadLocal引发的问题

1.ThreadLocal内存泄漏

在这里插入图片描述

  • ThreadLocal的生命周期结束。如果栈内存中的非静态变量ThreadLocal引用,方法调用结束后,回收栈内存ThreadLocal引用,此时只有key弱引用指向ThreadLocal对象,下次垃圾回收,会立刻将弱引用 对象回收
  • key变成了null,但是value还是强引用,对象还在堆里,无法使用,造成了内存泄露
  • jdk设计这做了优化,调用get/set/remove的时候,自动把keynull的东西删除了,以便下一次GCvalue回收掉

ThreadLocal实例通常是作为类的静态成员变量存在的
调用remove()在垃圾回收的时候,及时将Entry清理掉。

2.线程复用到导致ThreadLocal污染

  • 使用线程池的场景中,线程会被复用。
  • ThreadLocal是为每个线程提供独立的变量副本,以实现线程间的数据隔离。
  • 当线程被复用后,如果前一个任务在ThreadLocal中设置了某些值,而后续任务没有正确清理,就可能会出现 “污染” 的情况。
private static ThreadLocal<Integer> pageNum = new ThreadLocal<>();

public static void main(String[] args) {
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            1,  // 核心线程数为1
            1,  // 最大线程数为1
            1L, // 线程存活时间1分钟
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(5)  // 等待队列大小为5
    );
    threadPool.submit(() -> {
        pageNum.set(1);
        System.out.println("Task 1: User ID = " + pageNum.get());//Task 1: User ID = 1
        //  清理ThreadLocal
		//	pageNum.remove();
    });
    threadPool.submit(() -> {
        // 线程复用
        System.out.println("Task 2: User ID = " + pageNum.get());//Task 2: User ID = 1
    });
    // 关闭线程池
    threadPool.shutdown();
}
  • 在每个任务结束后,及时清理ThreadLocal中的值,这样可以避免前一个任务的值对后续任务产生影响。
  • 分页插件会在查询后自动清理分页参数,如果PageHelper.startPage设置参数后未跟查询或手动清理分页参数,出现线程数据污染,这种情况可以使用PageHelper.clearPage()手动清理

四、思考

1.ThreadLocal 定义为静态变量优点?

  • 避免频繁创建和销毁:非静态的ThreadLocal变量会随着对象的创建和销毁而产生和消失。如果在一个频繁创建和销毁对象的场景中使用非静态ThreadLocal,会导致大量的ThreadLocal对象的创建和销毁操作。
  • 静态变量被整个类所共享。方便跨方法传递数据,在没有ThreadLocal的情况下,如果一个线程需要共享数据时需要方法传递,方法调用层次的深和参数数量多,这种参数传递方式可能会变得非常复杂和混乱。
  • 在使用线程池的场景下,线程会被复用,方便存取。静态ThreadLocal可以让线程在多次复用过程中方便地设置和获取数据。当一个线程再次被分配任务时,它可以通过之前的静态ThreadLocal来获取之前存储的数据,或者设置新的数据来覆盖旧的数据【使用分页插件时,某个线程多次调用PageHelper.startPage方法,而不清除,只会覆盖分页参数】
  • ThreadLocal使用弱引用本意是内存回收;但业务中常常定义为一个全局静态常量,导致了弱引用失去了他本身的作用

2.ThreadLocal线程隔离性?

每个线程都有自己独立ThreadLocalMap来存储该线程对应的ThreadLocal变量。

参考:
1.ThreadLocal源码
2.面试官:ThreadLocal为什么会导致内存泄漏?如何解决的?
3.阿里二面:谈谈ThreadLocal的内存泄漏问题?问麻了。。。。
4.内存泄漏和内存溢出的区别和联系
5.ThreadLocal内存泄漏问题
6.强引用、弱引用、软引用和虚引用
7.什么是内存溢出,内存泄漏?以及解决方案
8.ThreadLocal的使用及其原理

🍉文章不定期持续更新,如果我的文章对你有帮助请 关注⚡ 点赞👍 收藏⭐️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值