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;
/** 省略其他代码*/
}
本文着重讨论 threadLocals
这个变量。以及这个 ThreadLocal
是如何规避内存泄漏的。
每一个Thread
对象都有这个 threadLocals
变量 ,它存储了当前线程中所有 ThreadLocal
对象,及其对应的值。
在 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方法。至于原理,请接着往下看!
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();
}
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;
}
}
}
其实就是调用了 ThreadLocalMap
的 set
方法。具体内容如下:
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);
}
}
其实就是调用了 ThreadLocalMap
的 remove
方法。具体内容如下:
public void remove() {
// 获取当前线程的 threadLocals 变量,类型为 ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
程序在申请内存以后,不能释放已经申请的内存空间。在Java中的体现就是,不会再被使用到的对象或变量,他们一直占用着内存。在多次泄漏后会报出OOM
。
在我们的线程栈的 ThreadLocal
引用 和 堆中的ThreadLocal
对象之间,没有了强引用之后,只要发生GC
,ThreadLocal
对象就会被回收,与此同时 Entry
中的key
也会被置为null
。如果在这时, Entry
中的 value
还保持着强引用,那就只能等待当前线程执行结束,当前线程的引用和当前线程对象被回收时,它才能被回收。也就是说当前线程如果迟迟不结束(比如线程池的线程复用),那么这个变量value就不会被回收,我们称这种情况为ThreadLocal
的内存泄漏。
当 key 为强引用时,ThreadLocalMap 就拥有了 ThreadLocal 的强引用,即便我们切断了线程栈方面的强引用,这个entry 中的key也是回收不掉的。除非手动设置删除。那么这种情况就会导致 Entry 的内存泄漏。
当 key 为弱引用时,ThreadLocalMap 就拥有了 ThreadLocal 的弱引用。当我们切断了线程栈方面的强引用,这个 entry中的key会在下次GC时被回收。此时,key就是 null,在我们下一次调用ThreadLocalMap 的 get、set、remove方法时,会自动删除 value,就安全了。一般我们建议使用remove方法删除value。
每次使用完ThreadLocal 都调用它的remove方法清除数据。
类似于GC ROOT 一样的存在。
将ThreadLocal定义为 private final static
,这样就能保证身为弱引用的key会一直在(可以通过ThreadLocal的弱引用访问Entry的value值,随后清除掉)。
我当前的Java是11版本,没有自带的 Visual VM 工具,需要自行安装。
安装插件:Visual VM
下载地址
太慢了的话,可以使用百度云下载 提取码:qhjs
IDEA中再安装插件 VisualVM Launcher
再配置你的运行:
配置你自己的exe文件
然后启动你的项目,Visual VM就会自动弹出来。
随后安装 Visual中的插件 Visual GC:
点击弹出来的界面中的 Tools -> Plugins
选择 Visual GC,并点击 Install。
至此准备工作就完毕了。
只 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时)
手动进行垃圾回收(点击Perform GC时),发现空间是回收了的。
这种情况需要是给 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中能看到老年代回收不了多少东西。
使用 set 后,最终 remove。可以有效避免内存泄漏。
代码验证仍然使用 5.2小节的代码。打开场景3的注释,同时注释掉其他场景。
// 测试场景3:set并remove,不会内存泄漏
ThreadLocal<Demo> threadLocal = new ThreadLocal<>();
threadLocal.set(new Demo());
threadLocal.remove();
观察到的情况是:
未手动进行垃圾回收(没点击 Perform GC时)