1、简介
ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰
这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
1.1 主要方法
T initialValue(): 初始化
void set(T t): 为这个线程设置一新值
T get(): 得到这个线程对应的value。如果是首次调用get()。则会调用initialize来得到这个值
void remove(): 删除这个线程得到的值
1.2 ThreadLocal与Synchronized的区别
ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别:
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问,它用于在多个线程间通信时能够获得数据共享。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
1.3 ThreadLocal内部设计
JDK8 之前的设计
每个ThreadLocal都创建一个ThreadLocalMap,用线程作为ThreadLocalMap的key,要存储的局部变量作为ThreadLocalMap的value,这样就能达到各个线程的局部变量隔离的效果
JDK8 之后的设计
每个Thread维护一个ThreadLocalMap,这个ThreadLocalMap的key是ThreadLocal实例本身,value才是真正要存储的值Object
每个Thread线程内部都有一个ThreadLocalMap
Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
JDK8对ThreadLocal这样改造的好处
减少ThreadLocalMap存储的Entry数量:因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量
当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用(但是不能避免内存泄漏问题,解决内存泄漏问题应该在使用完后及时调用remove()对ThreadMap里的Entry对象进行移除,由于Entry继承了弱引用类,会在下次GC时被JVM回收)
2、使用场景
ThreadLocal 适用于如下两种场景
每个线程需要有自己单独的实例。
实例需要在多个方法中共享,但不希望被多线程共享。
具体场景
存储用户Session
数据库连接,处理数据库事务
数据跨层传递(controller,service, dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
3、内存溢出
3.1 什么是内存溢出?
内存溢出(Out Of Memory,简称 OOM)是指无用对象(不再使用的对象)持续占有内存,或无用对象的内存得不到及时释放,从而造成的内存空间浪费的行为就称之为内存泄露。
3.2示例代码
在代码中我们会创建一个大对象,这个对象中会有一个 10m 大的数组,然后我们将这个大对象存储在 ThreadLocal 中,再使用线程池执行大于 5 次添加任务,因为设置了最大运行内存是 50m,所以理想的情况是执行 5 次添加操作之后,就会出现内存溢出的问题
@Slf4j
public class ThreadLocalOOMTest {
/**
* 定义一个 10m 大的类
*/
static class MyTask {
// 创建一个 10m 的数组(单位转换是 1M -> 1024KB -> 1024*1024B)
private byte[] bytes = new byte[10 * 1024 * 1024];
}
// 定义 ThreadLocal
private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 1、创建线程池
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(5, 5, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
// 2、执行 10 次调用,建立10个线程
for (int i = 0; i < 10; i++) {
// 执行任务
executeTask(threadPoolExecutor);
//休眠1秒
Thread.sleep(1000);
}
}
/**
* 线程池执行任务
*
* @param threadPoolExecutor
* @methodName: executeTask
* @return: void
* @author: ybw
* @date: 2023/2/24
**/
private static void executeTask(ThreadPoolExecutor threadPoolExecutor) {
// 执行任务
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
log.info("创建对象");
// 创建对象(10M)
MyTask myTask = new MyTask();
// 存储 ThreadLocal
taskThreadLocal.set(myTask);
// 将对象设置为 null,表示此对象不在使用了
myTask = null;
}
});
}
}
配置idea,程序运行的最大内存设置为 50m
-Xms50m -Xmx50m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/1/dump2.hprof
运行后,日志如下
[INFO ] 2023-02-24 18:11:49.338 [pool-1-thread-1] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
[INFO ] 2023-02-24 18:11:50.344 [pool-1-thread-2] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
[INFO ] 2023-02-24 18:11:51.356 [pool-1-thread-3] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
[INFO ] 2023-02-24 18:11:52.367 [pool-1-thread-4] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
[INFO ] 2023-02-24 18:11:53.378 [pool-1-thread-5] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:/1/dump2.hprof ...
Unable to create D:/1/dump2.hprof: File exists
Exception in thread "pool-1-thread-5" java.lang.OutOfMemoryError: Java heap space
at com.ybw.arthas.demo.memory.ThreadLocalOOMTest$MyTask.<init>(ThreadLocalOOMTest.java:26)
at com.ybw.arthas.demo.memory.ThreadLocalOOMTest$1.run(ThreadLocalOOMTest.java:62)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
[INFO ] 2023-02-24 18:11:54.385 [pool-1-thread-1] c.y.a.demo.memory.ThreadLocalOOMTest - 创建对象
Exception in thread "pool-1-thread-1" java.lang.OutOfMemoryError: Java heap space
at com.ybw.arthas.demo.memory.ThreadLocalOOMTest$MyTask.<init>(ThreadLocalOOMTest.java:26)
at com.ybw.arthas.demo.memory.ThreadLocalOOMTest$1.run(ThreadLocalOOMTest.java:62)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
从上述日志可看出,当程序执行到第 5 次添加对象时就出现内存溢出的问题了,这是因为设置了最大的运行内存是 50m,每次循环会占用 10m 的内存,加上程序启动会占用一定的内存,因此在执行到第 5 次添加任务时,就会出现内存溢出的问题。
3.3 内存溢出原因定位(通过hprof文件)
3.2 代码执行后,会生成dump2.hprof文件,将dump2.hprof装入jvisualvm(jdk自带的监测、故障处理工具)。
基本信息里面有异常错误的线程。
在“堆转储上的线程”可以看到标红的异常错误的线程,里面有错误代码及出错行号。
也可以查找“最大的对象”,如下图查出前20个最大的对象
点进入后,可以看到具体内容,可以看到占用最多的是byte
3.4 内存溢出原因分析
我们首先打开 set 方法的源码(在示例中使用到了 set 方法)
/**
* 设置当前线程对应的ThreadLocal的值
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry,this这里指调用此方法的ThreadLocal对象
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从上述代码我们可以看出 Thread、ThreadLocalMap 和 ThreadLocal.set 方法之间的关系:每个线程 Thread 都拥有一个自己的数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的value放到 ThreadLocalMap 容器中。
接下来我们再看一下 ThreadLocalMap 的源码:
static class ThreadLocalMap {
// 实际存储数据的数组
private Entry[] table;
// 存数据的方法
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();
// 如果有对应的 key 直接更新 value 值
if (k == key) {
e.value = value;
return;
}
// 发现空位插入 value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建一个 Entry 插入数组中
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ... 忽略其他源码
}
从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。因为key都是相同的,所以table是只有一个元素的数组。
它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。
4、解决方案
ThreadLocal 内存溢出的解决方案很简单,我们只需要在使用完 ThreadLocal 之后,执行 remove 方法就可以避免内存溢出问题的发生了。
remove源码如下:
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
当调用了 remove 方法之后,会直接将 Thread 中的 ThreadLocalMap 对象移除掉,这样 Thread 就不再持有 ThreadLocalMap 对象了,所以即使 Thread 一直存活,也不会造成因为(ThreadLocalMap)内存占用而导致的内存溢出问题了。