likes
comments
collection
share

ThreadLocal如何避免内存泄漏

作者站长头像
站长
· 阅读数 31

ThreadLocal简介

ThreadLocal 是 Java 中的一个线程本地存储机制,它允许每个线程拥有一个独立的本地存储空间,用于存储该线程的变量。ThreadLocal 提供了一种简单的方式来解决多线程环境下共享变量的问题,避免了在多线程环境下出现的线程安全问题。

欢迎关注个人公众号【好好学技术】交流学习

ThreadLocal简单用法

public class ThreadLocalDemo {  
    private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);  
    
    //当前值+1
    public static void increment() {  
        int value = threadLocal.get();  
        threadLocal.set(value + 1);  
    }  

    public static void main(String[] args) throws InterruptedException {  
        for (int i = 0; i < 10; i++) {  
            new Thread(() -> {  
                String threadName = Thread.currentThread().getName();  
                increment();  
                System.out.println(threadName + " 当前threadLocal的值为:" + threadLocal.get());  
            }).start();  
        }  
    }  
  
}

输出结果为

Thread-0 当前threadLocal的值为:1
Thread-5 当前threadLocal的值为:1
Thread-3 当前threadLocal的值为:1
Thread-4 当前threadLocal的值为:1
Thread-6 当前threadLocal的值为:1
Thread-2 当前threadLocal的值为:1
Thread-9 当前threadLocal的值为:1
Thread-1 当前threadLocal的值为:1
Thread-7 当前threadLocal的值为:1
Thread-8 当前threadLocal的值为:1

我们发现每个线程虽然都共享同一个threadLocal实例,但它们并没有发生相互干扰的情况,而是各自产生独立的值,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

使用场景

spring事务模板类

//使用示例
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

//查看源码
public static TransactionStatus currentTransactionStatus() throws NoTransactionException {  
    TransactionInfo info = currentTransactionInfo();  
    if (info == null || info.transactionStatus == null) {  
        throw new NoTransactionException("No transaction aspect-managed TransactionStatus in scope");  
    }  
    return info.transactionStatus;  
}

//currentTransactionInfo方法
@Nullable  
protected static TransactionInfo currentTransactionInfo() throws NoTransactionException {  
    return transactionInfoHolder.get();  
}

//这里使用了ThreadLocal
private static final ThreadLocal<TransactionInfo> transactionInfoHolder =  
new NamedThreadLocal<>("Current aspect-driven transaction");

HttpServletRequest

项目中要获取当前HttpServletRequest可以使用

HttpServletRequest request =  
((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

我们来看看RequestContextHolder.getRequestAttributes()方法

@Nullable  
public static RequestAttributes getRequestAttributes() {  
    RequestAttributes attributes = requestAttributesHolder.get();  
    if (attributes == null) {  
        attributes = inheritableRequestAttributesHolder.get();  
    }  
    return attributes;  
}

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =  
new NamedThreadLocal<>("Request attributes");  
  
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =  
new NamedInheritableThreadLocal<>("Request context");

aop调用链传递

LCN/seata都是使用ThreadLocal传递调用链的,这里就不展开讲了。

内存泄漏与内存溢出

内存泄漏

内存泄漏指的是程序中存在某些对象或资源没有被妥善地释放,导致这些对象或资源一直占用着内存,而无法被回收。随着时间的推移,这些未释放的对象或资源会越来越多,最终耗尽系统的内存资源,导致系统崩溃。

常见的内存泄漏包括:

  • 对象被创建后,没有及时被销毁,成为垃圾对象。
  • 没有正确关闭IO资源。
  • 缓存没有被清空。
  • 静态集合类对象未删除引用。
  • 单例模式下对象未及时释放等。

内存溢出

内存溢出指的是程序在申请内存时,无法获得足够的内存空间,导致程序无法正常运行。通常情况下,当程序需要使用的内存超过了系统能够提供的内存时,就会发生内存溢出。

常见的内存溢出包括:

  • 堆内存溢出:由于创建了过多的对象或者某些对象太大,导致堆内存不足。
  • 栈内存溢出:由于方法调用过多或者某些方法的递归调用层数过多,导致栈内存不足。
  • 永久代内存溢出:由于创建了过多的类或者字符串,导致永久代内存不足。

区别

内存泄漏和内存溢出的区别在于它们发生的原因和表现形式。内存泄漏是指对象或者资源无法被妥善释放,导致系统资源浪费,而内存溢出则是指系统不能分配所需内存,导致程序崩溃或者异常。通常情况下,内存泄漏会逐渐消耗系统资源,而内存溢出则是突然发生的。

解决内存泄漏的方法是找到未被正确释放的对象或资源,并手动进行释放。而解决内存溢出的方法则需要优化程序代码,减少内存使用量,或增加系统内存大小等方式来解决。

java强软弱虚

Java中的引用类型有四种:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。它们之间的主要区别在于对象被垃圾回收时的行为不同。

强引用

强引用是默认类型的引用,当我们通过“new”关键字创建一个对象时,该对象会被分配到堆内存中,并且默认情况下,该对象的引用是强引用。只要强引用存在,垃圾回收器就不会将其回收。

例如:

Object obj = new Object();

在上面的代码中,obj是一个强引用,因此只有当obj变量被显示地设置为null时,才能使对象成为垃圾,等待垃圾回收器收集。

软引用

软引用可以让对象存活更长时间,直到内存不足时才回收它。如果垃圾回收器需要更多的内存,则会回收只被软引用引用的对象。当一个对象只被软引用引用时,它会被保留在内存中,直到系统内存不够用或者垃圾回收器需要更多空间为止。通过软引用可以实现一些缓存功能。

例如:

SoftReference<Object> softRef = new SoftReference<>(new Object());

在上面的代码中,softRef是一个软引用,当垃圾回收器需要内存时,它可以将该对象回收,并释放所占用的内存。

弱引用

弱引用比软引用生命期更短,当一个对象只被弱引用引用时,当垃圾回收器运行时,不管当前内存是否充足,都会将其回收。弱引用通常用于实现缓存机制或者观察者模式。

例如:

WeakReference<Object> weakRef = new WeakReference<>(new Object());

在上面的代码中,weakRef是一个弱引用,这意味着垃圾回收器可以随时将该对象回收,而无需考虑系统内存是否充足。

虚引用

虚引用也称为幽灵引用,与其他三种引用方式不同,它并不会决定对象是否能存活。如果一个对象只有虚引用,那么就像没有任何引用一样,它的内存会被回收,但是在回收之前会调用finalize()方法。虚引用主要用于管理DirectBuffer的生命周期。

例如:

PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), null);

在上面的代码中,phantomRef是一个虚引用,当垃圾回收器发现该对象的内存已被回收时,它会将其插入队列中,并在下一次调用垃圾回收器时通知引用对象被回收了。

ThreadLocal原理

ThreadLocal

ThreadLocal是一个泛型类,它提供了get()、set()和remove()方法来获取、设置和删除当前线程的变量副本。它的原理是在每个Thread对象中都有一个ThreadLocalMap类型的私有变量threadLocals,该变量存储着当前线程所对应的所有ThreadLocal变量的值。

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;  
        }  
    }  
    return setInitialValue();  
}

public void set(T value) {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        map.set(this, value);  
    else  
        createMap(t, value);  
}

public void remove() {  
    ThreadLocalMap m = getMap(Thread.currentThread());  
    if (m != null)  
        m.remove(this);  
}

ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,它实际上就是一个HashMap,用于存储当前线程所对应的所有ThreadLocal变量的值。每个ThreadLocal对象都会被保存在ThreadLocalMap中,并且使用ThreadLocal作为key来访问它的变量值。这样做的好处是每个线程都可以独立维护自己的数据,而不会与其他线程产生冲突。

ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}

ThreadLocal.ThreadLocalMap threadLocals = null;

这里需要注意的是ThreadLocalMap每个元素都是Entry,而它是用弱引用对象作为key存储在ThreadLocalMap中


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

    Entry(ThreadLocal<?> k, Object v) {  
        super(k);  
        value = v;  
    }  
}

实现原理

当我们通过ThreadLocal类创建一个新的变量时,实际上是在当前线程的threadLocals变量中创建了一个新的Entry对象,该对象的key是ThreadLocal对象本身,value则是我们设置的变量值。这个Entry对象会存储在ThreadLocalMap中。

当我们需要在当前线程中访问这个变量时,ThreadLocal会根据当前线程获取对应的ThreadLocalMap对象,并根据ThreadLocal对象作为key来查找该变量的值。由于每个线程都有自己独立的ThreadLocalMap对象,因此不同线程之间的变量互不干扰。

ThreadLocal内存泄漏原因

每个ThreadLocal对象都会被存储在当前线程的ThreadLocalMap中,并且使用ThreadLocal对象作为key来访问它的变量值。由于使用的是弱引用对象作为key,当一个ThreadLocal对象没有被任何线程引用时,该对象就会被回收。

但是,即使ThreadLocal对象已经被回收,对应的变量副本仍然存在于该线程的ThreadLocalMap中。这是因为ThreadLocalMap内部使用了强引用对象(Entry对象)来引用变量副本,只有在当前线程被回收时,ThreadLocalMap中对应的Entry才会被回收。

也就是说,ThreadLocal对象虽然使用的是弱引用,但是与之关联的变量副本却是通过强引用对象间接引用的,因此在ThreadLocal对象被回收后,其变量副本可能不会立刻被回收。如果我们没有手动调用remove()方法将变量副本从ThreadLocalMap中清除,那么它就会一直存在于内存中,从而导致内存泄漏问题。

ThreadLocal内存泄漏常见场景

ThreadLocal内存泄漏的原因主要是由于线程复用导致的。

线程池

当我们使用线程池时,如果在线程中使用了ThreadLocal变量,那么该变量并不会被自动清除。线程池中的线程是可以被重复利用的,如果我们在一个线程中使用了ThreadLocal变量,并且没有在该线程结束前手动清除它,那么这个变量将会一直存在于ThreadLocalMap中,即使该线程已经被回收,这就会导致内存泄漏。

长时间持有

如果我们在一个线程中创建了ThreadLocal变量,并且一直持有它却不使用,这也会导致内存泄漏问题。在这种情况下,由于该变量一直存在于ThreadLocalMap中,即使该线程已经被回收,该变量也无法被释放,最终会导致内存泄漏。

ThreadLocal避免内存泄漏方法

为了避免ThreadLocal内存泄漏问题,我们可以采取以下措施:

  1. 在使用完ThreadLocal变量后,应该尽快调用remove()方法将其从ThreadLocalMap中清除,以便让垃圾回收器回收它们。
  2. 将ThreadLocal变量定义成private static类型的,并且在使用完之后手动清除,以避免线程重用时引起的内存泄漏问题。
  3. 不要在线程池中使用ThreadLocal变量,如果必须使用,应该在使用完后手动清理。

总结

虽然ThreadLocal可以解决线程安全问题,但是在使用完之后需要手动清除,以避免线程重用时引起的内存泄漏问题。

转载自:https://juejin.cn/post/7234365864842297401
评论
请登录