• Java进阶篇--并发容器之ThreadLocal内存泄漏


    目录

    ThreadLocal内存泄漏的原因?

    改进和优化

    cleanSomeSlots方法

    expungeStaleEntry方法

    replaceStaleEntry方法

    为什么使用弱引用?

    Thread.exit()

    ThreadLocal内存泄漏最佳解决方案

    在使用完毕后立即清理ThreadLocal

    使用InheritableThreadLocal替代ThreadLocal

    使用弱引用清理ThreadLocal


    ThreadLocal内存泄漏的原因?

    ThreadLocal是为了解决多线程共享访问对象带来的线程安全问题。它通过为每个线程分配一个对象实例,达到隔离的目的,使得线程之间互不影响。与同步机制不同的是,同步机制以时间换空间,控制线程访问共享对象的顺序,而ThreadLocal则是为每个线程分配一个对象实例,牺牲了空间效率换来时间效率。但是,在ThreadLocal使用过程中存在内存泄漏的风险,如果线程执行结束后,ThreadLocal,ThreadLocalMap,entry都会被回收掉,但在线程池中,线程是复用的,所以ThreadLocal的内存泄漏就值得我们关注。

    ThreadLocal内存泄漏的原因主要是因为在使用ThreadLocal时没有及时清理ThreadLocal对象所引用的线程特有的副本。具体来说,当一个线程结束后,如果没有手动清理或者调用remove方法来移除对应的ThreadLocal对象,那么这个ThreadLocal对象仍然会被ThreadLocalMap持有,而ThreadLocalMap是通过弱引用来关联ThreadLocal对象的,如果ThreadLocal对象没有被其他强引用持有,那么在垃圾回收的时候就会被回收,但是对应的线程特有的副本却无法被回收,从而导致内存泄漏。

    另外,如果使用线程池来管理线程,线程池中的线程是会被复用的,而不会在每次任务执行结束后销毁线程。这就意味着线程池中的线程仍然持有之前任务中创建的ThreadLocal对象,而这些对象对应的线程特有的副本却不会被释放,从而导致内存泄漏的问题。

    改进和优化

    对于ThreadLocal内存泄漏的问题,Java在不同版本中进行了不同的改进和优化。以下是一些改进措施:

    cleanSomeSlots方法

    cleanSomeSlots方法的改进: 在JDK 6之前,ThreadLocalMap中没有自动清理过期Entry的机制。JDK 7引入了cleanSomeSlots方法来解决这个问题。每次调用set或get方法时,会以一定的概率触发该方法,该方法会遍历整个表格,并清理掉过期的Entry。这样可以减轻内存泄漏的风险,使得那些已经过期且无法再被访问的线程特有副本得到释放。

    1. public class MyThreadLocal extends ThreadLocal {
    2. @Override
    3. protected T initialValue() {
    4. // 初始化方法
    5. return ...;
    6. }
    7. @Override
    8. public void set(T value) {
    9. super.set(value);
    10. cleanSomeSlots();
    11. }
    12. @Override
    13. public T get() {
    14. T value = super.get();
    15. cleanSomeSlots();
    16. return value;
    17. }
    18. private void cleanSomeSlots() {
    19. ThreadLocalMap map = getMap(Thread.currentThread());
    20. if (map != null) {
    21. map.cleanSomeSlots();
    22. }
    23. }
    24. }

    expungeStaleEntry方法

    expungeStaleEntry方法的改进: JDK 8引入了expungeStaleEntry方法,该方法用于显式地清理过期的Entry。在ThreadLocalMap的size超过阈值时被调用,该方法会遍历整个表格,将key为null的Entry移除以释放关联的线程特有副本。

    1. public class MyThreadLocal extends ThreadLocal {
    2. @Override
    3. protected T initialValue() {
    4. // 初始化方法
    5. return ...;
    6. }
    7. @Override
    8. public void set(T value) {
    9. super.set(value);
    10. expungeStaleEntry();
    11. }
    12. @Override
    13. public T get() {
    14. T value = super.get();
    15. expungeStaleEntry();
    16. return value;
    17. }
    18. private void expungeStaleEntry() {
    19. ThreadLocalMap map = getMap(Thread.currentThread());
    20. if (map != null) {
    21. map.expungeStaleEntry();
    22. }
    23. }
    24. }

    replaceStaleEntry方法

    replaceStaleEntry方法的改进: JDK 9引入了replaceStaleEntry方法,用于在创建新的Entry时替换已经过期的Entry。该方法主要解决了JDK 8中可能出现的并发问题,保证在替换Entry时不会有其他线程同时访问旧的Entry,从而避免了可能的内存泄漏。

    1. public class MyThreadLocal extends ThreadLocal {
    2. @Override
    3. protected T initialValue() {
    4. // 初始化方法
    5. return ...;
    6. }
    7. @Override
    8. public void set(T value) {
    9. super.set(value);
    10. replaceStaleEntry();
    11. }
    12. @Override
    13. public T get() {
    14. T value = super.get();
    15. replaceStaleEntry();
    16. return value;
    17. }
    18. private void replaceStaleEntry() {
    19. ThreadLocalMap map = getMap(Thread.currentThread());
    20. if (map != null) {
    21. map.replaceStaleEntry();
    22. }
    23. }
    24. }

    为什么使用弱引用?

    使用弱引用主要是为了解决ThreadLocal中的内存泄漏问题。在线程局部变量中,如果使用强引用,即使在业务代码中将ThreadLocal实例设置为null,由于Entry强引用着ThreadLocal,ThreadLocal对象无法被垃圾回收,从而导致内存泄漏。

    而使用弱引用修饰ThreadLocal可以解决这个问题。当ThreadLocal实例不再被业务代码使用时,由于ThreadLocalMap中使用了弱引用来引用ThreadLocal实例,ThreadLocal实例会在下一次垃圾回收时被正确地回收掉。同时,在ThreadLocal的生命周期中会对key为null的脏entry进行处理,避免出现潜在的内存泄漏。

    尽管使用弱引用会导致可能出现一些内存泄漏问题,但相比起使用强引用造成的内存泄漏,弱引用的使用能够保证在ThreadLocal的生命周期内尽可能地避免内存泄漏问题,从而提高应用的安全性和可靠性。

    需要注意的是,虽然使用弱引用可以减少内存泄漏的潜在问题,但仍然需要在使用ThreadLocal时注意及时清理和移除不再使用的ThreadLocal实例,以确保整体系统的资源利用效率。

    Thread.exit()

    Thread.exit()方法是一个废弃的方法,不推荐使用。它会导致线程突然终止,可能会破坏线程的稳定性和数据完整性,并且无法保证所有资源的正确释放。在正常情况下,应该通过执行完任务或者正常结束的方式让线程退出。如果需要强制终止线程,可以通过调用Thread的interrupt方法来进行管理和控制。

    1. public class InterruptExample {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while (!Thread.currentThread().isInterrupted()) {
    5. // 执行线程的任务
    6. // ...
    7. // 检查中断标志
    8. if (Thread.currentThread().isInterrupted()) {
    9. System.out.println("线程被中断,退出循环");
    10. break;
    11. }
    12. }
    13. System.out.println("线程退出");
    14. });
    15. thread.start();
    16. // 给线程发送中断信号
    17. thread.interrupt();
    18. }
    19. }

    在这个示例中,线程在while循环中执行任务,并在每次循环开始时检查中断标志。如果中断标志被设置,线程会退出循环并输出相应信息。

    在main方法中,我们使用thread.interrupt()方法给线程发送中断信号。这会将线程的中断标志设置为true。线程在下一次循环开始时会检查到这个中断标志,并做出相应的处理来退出循环。

    这种方式可以安全地控制线程的退出,避免了Thread.exit()方法可能导致的问题。同时,它也提供了更灵活和可控的方式来管理线程的生命周期。

    ThreadLocal内存泄漏最佳解决方案

    由于ThreadLocal为每个线程维护一个独立的变量副本,因此如果没有及时清理ThreadLocal,可能会导致内存泄漏问题。下面是一些解决ThreadLocal内存泄漏问题的最佳实践:

    在使用完毕后立即清理ThreadLocal

    及时清理是防止内存泄漏的最佳解决方案之一。确保在使用完ThreadLocal后调用其remove()方法,清除数据。

    特别是在使用线程池的情况下,由于线程的复用性,如果没有清理ThreadLocal,可能会导致线程中保存的数据对后续线程产生干扰,进而导致业务逻辑出现问题。因此,类似于加锁与解锁一样,使用完ThreadLocal后就应该立即清理,以确保下次使用时不会受到上次使用遗留下来的数据的影响。

    1. public class UserContext {
    2. private static final ThreadLocal USER_THREAD_LOCAL = new ThreadLocal<>();
    3. public static void setUser(User user) {
    4. USER_THREAD_LOCAL.set(user);
    5. }
    6. public static User getUser() {
    7. return USER_THREAD_LOCAL.get();
    8. }
    9. public static void clear() {
    10. USER_THREAD_LOCAL.remove();
    11. }
    12. }

    在这个示例中,我们定义了一个静态的ThreadLocal变量USER_THREAD_LOCAL,并提供了setUser、getUser和clear方法,在使用完USER_THREAD_LOCAL后,可以调用clear方法清理ThreadLocal。

    通过及时清理ThreadLocal,可以有效避免内存泄漏问题,并确保数据在不同线程间的隔离性。

    使用InheritableThreadLocal替代ThreadLocal

    如果需要在父线程和子线程之间共享ThreadLocal变量,可以使用InheritableThreadLocal替代ThreadLocal。InheritableThreadLocal也是一种ThreadLocal,但它可以让子线程继承父线程的ThreadLocal变量副本,从而避免重复创建副本的问题。

    1. public class InheritableRequestContext {
    2. private static final InheritableThreadLocal REQUEST_ID = new InheritableThreadLocal<>();
    3. public static void setRequestId(String requestId) {
    4. REQUEST_ID.set(requestId);
    5. }
    6. public static String getRequestId() {
    7. return REQUEST_ID.get();
    8. }
    9. public static void clear() {
    10. REQUEST_ID.remove();
    11. }
    12. }

    在这个示例中,我们使用了InheritableThreadLocal来定义共享变量REQUEST_ID,并提供了setRequestId、getRequestId和clear方法,以便在线程间共享该变量。

    使用弱引用清理ThreadLocal

    使用弱引用来清理ThreadLocal。通过将ThreadLocal变量存储在WeakReference中,可以让垃圾回收器在需要释放内存时自动清理ThreadLocal变量。

    1. public class WeakRequestContext {
    2. private static final ThreadLocal> REQUEST_ID = new ThreadLocal<>();
    3. public static void setRequestId(String requestId) {
    4. REQUEST_ID.set(new WeakReference<>(requestId));
    5. }
    6. public static String getRequestId() {
    7. WeakReference ref = REQUEST_ID.get();
    8. return ref != null ? ref.get() : null;
    9. }
    10. public static void clear() {
    11. REQUEST_ID.remove();
    12. }
    13. }

    在这个示例中,我们使用了ThreadLocal和WeakReference来定义变量REQUEST_ID,并提供了setRequestId、getRequestId和clear方法。

    总之,为避免ThreadLocal内存泄漏问题,可以采用立即清理、使用InheritableThreadLocal和使用弱引用等多种解决方案。在具体场景中,可以根据实际情况选择最佳的解决方案。

  • 相关阅读:
    【Linux】[gdb]Linux环境下如何调试代码
    Codeforces Round #813 (Div. 2)
    什么,这年头还有人不知道404
    Jenkins自动化测试
    java线程状态与线程安全问题
    charles配置
    【视觉SLAM入门】7.2. 从卡尔曼滤波到扩展卡尔曼滤波,引入、代码、原理、实战,C++实现以及全部源码
    RepVGG论文详解
    进程,线程切换
    TypeScript基础之泛型介绍
  • 原文地址:https://blog.csdn.net/m0_74293254/article/details/133970080