小强最近在疯狂补习高并发的相关知识,但是在学到threadLocal时有点力不从心了,尤其是threadLocal的底层架构和如何导致内存泄漏,今天我们帮小强一把!!把这一块彻底聊清楚!!!
文章目录
1.threadLocal的前世今生
1.为什么要使用threadLocal
ThreadLocal,即线程本地变量,在类定义中的注释如此写This class provides thread-local variables。如果创建了一个ThreadLocal变量,在每次set的时候其实会设置到线程的本地内存,多个线程操作这个变量的时候,实际是在操作线程自己本地内存里面的变量,所以不会发生影响,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。属于空间换时间的解决线程安全问题的方案。
2.threadLocal和Synchonized的比较
ThreadLocal 和 Synchonized 都用于解决多线程并发访问。可是 ThreadLocal 与 synchronized 有本质的差别。synchronized 是利用锁的机制, 使共享资源某一时该仅仅能被一个线程访问。
ThreadLocal 则是副本机制,一个线程会存储当前变量的副本,线程与线程之间都是隔离的,所以此时不管有多少线程访问都是并发安全的。但是可能会有内存泄漏的风险。
3.使用场景
1.跨方法实现数据传递。
2.在web容器中,每个完整的请求周期会由一个线程来处理,因此可以在线程统一设置,但是并不是增加方法参数,想要用的时候直接获取即可。
3.在spring中用threadLocal来设计TransactionSynchronizationManager,利用切面实现了事务管理和数据访问的解耦,同时也保证了多线程情况下Connection安全问题。(这个在之后spring源码会讲到!!)
类的解释如下
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
2.ThreadLocal的使用
1.接口方法
ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除, 目的是为了减少内存的占用。
• protected Object initialValue()
protected T initialValue() {
return null;
}
用protected修饰,表示可以被子类覆写,这个方法的调用时机实在当调用get()方法的时候,此时还没有设置值,那么返回initialValue()方法里return的值。
2.常见使用
public class ThreadLocalDemo {
// 创建一个ThreadLocal变量,用于存储每个线程的本地变量
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 创建一个线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交三个任务到线程池
for (int i = 0; i < 3; i++) {
final int taskId = i;
executor.submit(() -> {
// 为当前线程设置ThreadLocal变量的值
threadLocal.set(taskId);
// 模拟任务执行
try {
Thread.sleep(1000); // 休眠1秒以模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前线程设置的ThreadLocal变量的值
System.out.println(Thread.currentThread().getName() + " 的 ThreadLocal 变量值为:" + threadLocal.get());
// 清理ThreadLocal变量,避免内存泄漏(在实际应用中,这一步可以根据需要来决定是否执行)
threadLocal.remove();
});
}
// 关闭线程池
executor.shutdown();
}
}
重头戏来了,我们先去看一下threadLocal的底层架构!!!
3.threadLocal的底层机制
1.线程模型图
从这个图可以看到,每一个线程都会维护一个threadLocalMap,threadLocalMap的key存的就是threadLocal的引用,value是设置的值。threadLocal实现线程本地关键正是由于每个线程维护了threadLocalMap变量。
2.ThreadLocal关键方法
public class ThreadLocal<T> {
//get方法
public T get() {
Thread t = Thread.currentThread();
//返回当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//在threadLocalMap中存的是Object,在这个地方统一对泛型做转换
T result = (T)e.value;
return result;
}
}
//对于首次获取来讲,map是为空的,因此会去调用初始化方法setInitialValue
return setInitialValue();
}
//设置初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//其实就是当前调用get()的对象
map.set(this, value);
else
//在当前线程中创建ThreadLocalMap
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//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);
}
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[] 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();
}
}
3.ThreadLocalMap
ThreadLocalMap是一个声明在ThreadLocal的静态内部类,同时他的引用会在thread中维护。
ThreadLocalMap的结构
//
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//类似于key-value的格式,v为Object
//因为自己定义的ThreadLocal是可以接收任意类型的,只要在拿的时候根据泛型做转换即可
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//因为可能有多个变量需要现成隔离访问
private Entry[] table;
}
接下来,我们去看下最重要的一个问题,threadLocal如何导致内存泄漏??
4.threadLocal内存泄漏
1.在什么情况下会出现内存泄漏
在实际项目开发中,一般都是使用线程池来创建任务,但是线程池最大的特点是线程池中的核心线程在执行完任务后,是不会退出的,可以循环使用。那此时,比如你在用线程池执行任务的时候,用threadLocal设置了一个值,但是运行完之后,并没有remove,当第二个任务来的时候,又用了这个线程,但是用的是另外一个threadLocal。那这种情况下之前的threadLocal会一直存在。
举个例子,大家仔细看下:
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
threadLocal.set(1);
});
Thread.sleep(1000);
executorService.execute(() -> {
threadLocal2.set(2);
Integer i = threadLocal2.get();
System.out.println(i);
});
}
}
上面的demo,用的线程池只有一个线程,确保内存泄漏能复现。
从上面的代码上看,执行第二个任务的时候,你会发现threadLocalMap里面存在第一次线程执行完遗留的threadLocal。可以dubug看下,结果如下:
你会发现,执行第二个任务的时候,threadLocalMap中还存在执行第一次任务遗留的threadLocal,那此时,
由于threadLocal是ThreadLocalMap的key,ThreadLocalMap和thread是同周期,因此,只要核心线程一直存在,Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 就会一直存在,但是ThreadLocalMap的key(threadLocal)由于继承了弱引用,所以 threadlocal 将会被 gc 回收,那此时的value是永远访问不到的,所以会存在内存泄漏
最好的做法是不在需要使用ThreadLocal 变量后,都调用它的 remove()方法,清除数据,为了让大家理解清晰一些,我把对象关联图画下:
从图中可以看出,一旦发生gc,key->threadLocal这条线就会断掉,key为null,那此时value就永远访问不到了。
2.为什么使用弱引用而不是强引用
强引用一定会发生内存泄漏,弱引用可能会发生内存泄漏,为什么呢?请往下看!!!
假如key 使用强引用:以上面的单线程线程池执行任务来看,第一次执行任务遗留的threadLocal肯定会到第二次执行任务的时候还存在,因此这部分遗留下来的 threadLocal就会发生内存泄漏。
假如key 使用弱引用: 对 ThreadLocal 对象实例的引用被被置为 null 了,由于ThreadLocalMap 持有 ThreadLocal 的弱引用, 即使没有手动删除, ThreadLocal 的 对象实例也会被回收。value 在下一次 ThreadLocalMap 调用 set,get ,remove 都 有机会被回收。
所以综合上述来讲
利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。 通过JVM 利用调用 remove 、get 、set 方法的时候,可以有效的去回收key为null的引用,减少内存泄漏的风险。当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,并没有去有调用 remove 、get 、set ,且线程没执行完,就有可能会有内存风险。