ThreadLocal原理、内存泄漏的验证

文章目录

  • 前言
  • 正文
    • 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原理、内存泄漏的验证_第1张图片

当我们手动将线程栈和堆空间实例的强引用去掉时,也就是代码中设置了 threadlocal=null。关系图就发生了变化:
ThreadLocal原理、内存泄漏的验证_第2张图片
此时的堆中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原理、内存泄漏的验证_第3张图片
在我们的线程栈的 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
ThreadLocal原理、内存泄漏的验证_第4张图片
再配置你的运行:
ThreadLocal原理、内存泄漏的验证_第5张图片
配置你自己的exe文件
ThreadLocal原理、内存泄漏的验证_第6张图片
然后启动你的项目,Visual VM就会自动弹出来。
随后安装 Visual中的插件 Visual GC:
点击弹出来的界面中的 Tools -> Plugins
选择 Visual GC,并点击 Install。
ThreadLocal原理、内存泄漏的验证_第7张图片

至此准备工作就完毕了。

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 threadLocal = new ThreadLocal<>();
                // threadLocal.set(new Demo());

                // 测试场景3:set并remove,不会内存泄漏
                // ThreadLocal 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时)
ThreadLocal原理、内存泄漏的验证_第8张图片
手动进行垃圾回收(点击Perform GC时),发现空间是回收了的。
ThreadLocal原理、内存泄漏的验证_第9张图片

5.3 验证内存泄漏情况

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

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

仍然使用 5.2小节 中的代码,注释掉场景1的代码,放开场景2的代码。
监控现象如下:
未手动进行垃圾回收(没点击 Perform GC时)
ThreadLocal原理、内存泄漏的验证_第10张图片
手动进行垃圾回收(点击Perform GC时),发现空间回收不掉。
ThreadLocal原理、内存泄漏的验证_第11张图片
只set并进行垃圾回收时,回收不掉空间。点击抽样器,查看内存。发现byte[]占用最高。
ThreadLocal原理、内存泄漏的验证_第12张图片
ThreadLocal原理、内存泄漏的验证_第13张图片
当前有很多个Demo没有回收掉。GC中能看到老年代回收不了多少东西。
ThreadLocal原理、内存泄漏的验证_第14张图片

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

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

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

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

ThreadLocal原理、内存泄漏的验证_第15张图片
手动进行垃圾回收(点击Perform GC时),发现空间是回收了的。
ThreadLocal原理、内存泄漏的验证_第16张图片

你可能感兴趣的:(java练习,java源码,java基础学习,java,threadlocal,内存泄漏,threadlocal原理,visulalvm)