当前位置:网站首页>ThreadLocal及其内存泄露分析

ThreadLocal及其内存泄露分析

2022-08-09 10:46:00 GeorgiaStar

ThreadLocal

ThreadLocal是一个线程本地变量,作用主要是做数据隔离。ThreadLocal中填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,每个线程维护自己的变量副本,多个线程互相不可见,因此多线程操作该变量不必加锁,适合不同线程使用不同变量值的场景。

数据隔离有什么用,用在哪些场景?

一个典型的应用场景就是Spring事务,事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个JDBC Connection,整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。对ThreadLocal的使用主要是在TransactionSynchronizationManager这个类里面,代码如下:

public abstract class TransactionSynchronizationManager {
    
	//线程绑定的资源,比如DataSourceTransactionManager绑定是的某个数据源的一个Connection
	//在整个事务执行过程中都使用同一个Jdbc Connection
	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");

	//......
}

关于使用ThreadLocal解决持久层线程安全问题,在Spring在SingleTon模式下的线程安全这篇博客中也有分析。其实很多场景的cookie、session等数据隔离都是通过ThreadLocal去做实现的。

ThreadLocal用法及源码分析

ThreadLocal使用示例代码如下:

ThreadLocal<String> localName = new ThreadLocal();
localName.set("abcde");
String name = localName.get();
localName.remove();

先初始化一个泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。线程间数据隔离的,就意味着别的线程使用get()方法是拿不到其他线程set的值的。

set(T value)、get()、remove()三大主要方法的源码如下:

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")
            T result = (T)e.value;
            return result;
        }
    }
    //没有就创建一个空的ThreadLocalMap对象
    return setInitialValue();
}

public void set(T value) {
    
	//获取当前线程对象
    Thread t = Thread.currentThread();
    //获取线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    	//没有就创建一个空的ThreadLocalMap对象
        createMap(t, value);
}

public void remove() {
    
 //获取当前线程的ThreadLocalMap对象
 ThreadLocalMap m = getMap(Thread.currentThread());
 if (m != null)
     m.remove(this);
}
/** * 获取线程的ThreadLocalMap对象 * @param t the current thread * @return the map */
ThreadLocalMap getMap(Thread t) {
    
    return t.threadLocals;
}

源码很简单,重点关注ThreadLocalMap类对象,ThreadLocalMap类是ThreadLocal类的内部类。ThreadLocalMap类对象要从线程Thread类中取到,是Thread类中名为threadLocals的成员变量。ThreadLocalMap类实际上是对自定义实现的Entry[]数组结构的封装,并非继承自原生Map类,Entry中的Key即是ThreadLocal变量本身,Value则是具体该线程中的变量副本值。

ThreadLocalMap getMap(Thread t) {
    
    return t.threadLocals;
}
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;

	//省略……

}

看到这里基本上可以找到ThreadLocal实现数据隔离的原理了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量(Entry数组)里面的,别人没办法拿到,从而实现了隔离

ThreadLocalMap底层结构

看源码可以发现,ThreadLocalMap并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表就是纯数组

static class ThreadLocalMap {
    
   
    static class Entry extends WeakReference<ThreadLocal<?>> {
    
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
    
            super(k);
            value = v;
        }
    }
    
    private static final int INITIAL_CAPACITY = 16;
    private Entry[] table;
    private int size = 0;
    private int threshold; // Default to 0
    //......
}

ThreadLocalMap数据结构大致如下图所示:

为什么要用数组,因为开发过程中一个线程可以有多个ThreadLocal来存放不同类型的对象,例如ThreadLocal<String>、ThreadLocal<Long>等,它们都放到当前线程的ThreadLocalMap里的,所以用数组来存

多个Thread使用ThreadLocal维护本线程中变量的过程如下图所示,虚线箭头代表弱引用,实线箭头代表强引用。

ThreadLocalMap的弱引用及其引起的内存泄露

每个Thread内部都维护一个ThreadLocalMap字典数据结构,字典的Key值是ThreadLocal,value即为私有对象T。在Spring MVC中,经常用ThreadLocal保存当前登陆用户信息,这样线程在任意地方都可以取到用户信息了。仔细看源码,就会发现ThreadLocalMap中Entry的Key即ThreadLocal对象是采用弱引用引入的。

ThreadLocal在保存变量的时候(即调用set方法存储一个变量时)会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

什么是弱引用?

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。


怎么解决这个value值的内存泄露问题呢,在线程本地变量使用完成后,我们只要记得在使用的最后用remove把值清空就好了。

ThreadLocal<String> threadLocal = new ThreadLocal();
try {
    
    threadLocal.set("abcde");
    //……
    String value = threadLocal.get();
    //……
} catch (Exception e) {
    
	e.printStack();
} finally {
    
    threadLocal.remove();
}

为什么ThreadLocalMap对Entry中的key值ThreadLocal要设计成使用弱引用?

ThreadLocal的设计比较晦涩难懂,究其原因是我们通过threadLocal对象的set方法进行存储值,但数据并不是存储在ThreadLocal对象中,而是存储在当前调用该方法的线程对象中。但从应用者的角度来看,我们操作的对象是ThreadLocal,从设计上来说就应该为它考虑。考虑一个问题:如果应用程序觉得ThreadLocal对象的使命完成,将threadLocal的引用设置为null,如果Entry中的key值的引用类型是强引用的话,会发生什么问题?答案是:ThreadLocal对象会无法被垃圾回收器回收,因为从thread对象出发,有强引用指向threadlocal obj。此时会违背用户的初衷,造成所谓的内存泄露。

JVM发现某个线程(Thread对象)中的threadLocals成员变量存在着对某个ThreadLocal对象的强引用,就不会回收那个ThreadLocal对象。因此,使用弱引用可以防止长期存在的线程(通常使用了线程池)导致ThreadLocal无法回收造成内存泄漏。虽然Key值(ThreadLocal)是通过弱引用引入的,但是value即变量值本身是通过强引用引入。这就导致假如不作任何处理,由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry

实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作,即如下expungeStaleEntry()方法:

//staleSlot代表那样key=null的Entry在数组中的下标。
private int expungeStaleEntry(int staleSlot) {
    
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
    
        ThreadLocal<?> k = e.get();
        if (k == null) {
    
            e.value = null;
            tab[i] = null;
            size--;
        } else {
    
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
    
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

上述方法的作用是擦除某个下标的Entry(置为null,可以回收),同时检测整个Entry[]表中对key为null的Entry一并擦除,重新调整索引。该方法在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作。但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。

ThreadLocal应用最佳实践

  • 在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理
  • ThreadLocal一般加static修饰,如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费

那么我们在使用ThreadLocal时,set方法执行后没有执行remove方法就一定会造成内存泄露甚至内存溢出吗?

在实际情况中,其实不调用remove方法并不那么容易造成内存溢出,因为从存储结构来看,除非创建海量线程,并且这些线程都不释放,导致大量线程内部持有的ThreadLocalMap对象一直不会释放,但一个线程所持有的Entry对象个数的多少,取决于关联的ThreadLocal对象个数,故我们需要的关注点而不是remove方法,而是防止线程资源泄露。

原网站

版权声明
本文为[GeorgiaStar]所创,转载请带上原文链接,感谢
https://blog.csdn.net/fuzhongmin05/article/details/108059645