当前位置: 首页 > article >正文

【JUC并发编程系列】深入理解Java并发机制:线程局部变量的奥秘与最佳实践(五、ThreadLocal原理、对象之间的引用)

文章目录

  • 【JUC并发编程系列】深入理解Java并发机制:线程局部变量的奥秘与最佳实践(五、ThreadLocal原理、对象之间的引用)
    • 1. 基本 API 介绍
    • 2. 简单用法
    • 3. 应用场景
    • 4. Threadlocal与Synchronized区别
    • 5. 内存溢出和内存泄漏
      • 5.2 内存溢出 (Memory Overflow)
      • 5.2 内存泄漏 (Memory Leak)
    • 6. 强引用、软引用、弱引用和虚引用
      • 6.1 强引用 (Strong Reference)
      • 6.2 软引用 (Soft Reference)
      • 6.3 弱引用 (Weak Reference)
      • 6.4 虚引用 (Phantom Reference)
      • 6.5 示例代码
    • 7. Threadlocal原理分析
    • 8. ThreadLocal 内存泄漏问题及避免方法
    • 9. ThreadLocal 的引用类型与内存泄漏问题


【JUC并发编程系列】深入理解Java并发机制:线程局部变量的奥秘与最佳实践(五、ThreadLocal原理、对象之间的引用)

ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,Threadloca适用于在多线程的情况下,可以实现传递数据,实现线程隔离。

java.lang.ThreadLocal 是 Java 中一个用于实现线程局部变量的类。它提供了一种在每个线程中拥有独立变量副本的机制,这样每个线程都可以独立地改变自己的副本,而不会影响其他线程中的副本。

1. 基本 API 介绍

ThreadLocal 类的构造方法

  • ThreadLocal(): 创建一个新的 ThreadLocal 实例。

ThreadLocal 的主要方法

  • T get(): 返回当前线程中该 ThreadLocal 的值。如果当前线程中还没有值,则会调用 initialValue() 方法来初始化这个值。
  • void set(T value): 设置当前线程中该 ThreadLocal 的值。
  • protected T initialValue(): 返回该 ThreadLocal 的初始值。默认返回 null。子类可以重写此方法以提供不同的初始值。

ThreadLocal 的高级特性

  • void remove(): 移除当前线程中该 ThreadLocal 的值。这有助于垃圾回收。

2. 简单用法

下面是一个简单的示例来说明如何使用 ThreadLocal:

public class Example {
    static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0; // 设置初始值为0
        }
    };

    public static void main(String[] args) {
        // 在主线程中设置值
        threadLocal.set(10);
        System.out.println("Main thread: " + threadLocal.get());

        // 创建新线程并设置不同的值
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set(20);
                System.out.println("Thread 1: " + threadLocal.get());
            }
        });

        t1.start();
        try {
            t1.join(); // 等待线程结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 主线程中的值没有改变
        System.out.println("Main thread after thread 1: " + threadLocal.get());
    }
}

在这个例子中,主线程和线程 t1 都有自己的 ThreadLocal 变量副本,并且它们之间互不影响。

注意事项

  • 使用 ThreadLocal 时要小心内存泄漏的问题。如果不显式调用 remove() 方法或不通过 set(null) 清除引用,那么线程的生命周期内该 ThreadLocal 的值将一直保留,可能导致内存泄漏。
  • 如果 ThreadLocal 不再需要,建议调用 remove() 方法释放资源。

3. 应用场景

1. Spring 事务管理

在 Spring 框架中,TransactionAspectSupport 提供了对事务的支持。其中有一个方法 currentTransactionStatus() 可以返回当前线程中的事务状态,通常用来决定事务是否应该回滚。

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

**应用场景:**当某个业务逻辑执行失败时,可以通过调用 setRollbackOnly() 方法来标记当前事务需要回滚,而不是提交。这会在事务结束时被事务管理器检测到并触发回滚操作。

**为何使用 ThreadLocal:**因为事务状态是每个线程独有的,所以在不同的线程中处理不同的事务时,需要确保事务状态不被混淆。ThreadLocal 提供了这种隔离性。

2. 获取 HttpServletRequest

在 Web 应用程序中,HttpServletRequest 对象通常在每个 HTTP 请求处理过程中可用。为了在不同的层次中访问这个对象,可以使用 RequestContextHolder

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

**应用场景:**在过滤器、拦截器或服务层等地方,可能需要访问 HTTP 请求的信息(如请求头、参数等)。

**为何使用 ThreadLocal:**每个 HTTP 请求都是在一个独立的线程中处理的。因此,RequestContextHolder 使用 ThreadLocal 来存储当前线程内的 HttpServletRequest 对象,确保每个线程只能访问与之相关的 HTTP 请求。

3. AOP 调用链传递参数

在分布式系统中,比如使用 LCN 或 Seata 这样的分布式事务框架时,需要在不同的服务间传递全局唯一的事务 ID。

**应用场景:**在发起一个分布式事务时,首先会生成一个全局唯一的事务 ID,然后通过 ThreadLocal 将这个 ID 保存在当前线程中。之后,在后续的调用中,可以从中获取这个 ID 并将其作为参数传递给下游服务。

**为何使用 ThreadLocal:**分布式事务通常涉及多个服务间的调用,每个服务调用都在单独的线程中执行。为了确保每个服务调用能够访问同一个事务 ID,使用 ThreadLocal 来存储这个 ID 是非常合适的。

总结来说,ThreadLocal 在这些场景下被广泛使用是因为它提供了每个线程的数据隔离,这对于保持线程安全和避免并发问题至关重要。特别是在涉及事务管理和跨层通信的情况下,ThreadLocal 的使用可以简化代码并减少潜在的错误。

4. 真实应用场景

例如:在aop中生成全局的id,目标方法获取aop中生成的全局id;

  • aop拦截目标方法,生成全局的id

  • doTest 如何获取在aop类中生成全局id呢

@Component
public class GlobalIDContextholder {

    private ThreadLocal<String> globalIds = new ThreadLocal<String>();

    public String getId() {
        return globalIds.get();
    }

    public void set(String globalId) {
        globalIds.set(globalId);
    }
}

@Aspect
@Component
@Slf4j
public class TestAop {
    @Autowired
    private GlobalIDContextholder globalIDContextholder;

    /**
     * 切入点
     */
    @Pointcut("execution(public * com.mayikt.service.*Service.*(..))")
    public void log() {
    }

    /**
     * 前置操作
     *
     * @param point 切入点
     */
    @Before("log()")
    public void beforeLog(JoinPoint point) {
        UUID globalId = UUID.randomUUID();
        globalIDContextholder.set(globalId.toString());
    }
}

@RestController
public class TsetController {
    @Autowired
    private GlobalIDContextholder globalIDContextholder;

    @GetMapping("/doTest")
    public String doTest() {
        return "从aop中获取的全局id:" + globalIDContextholder.getId();
    }
}

4. Threadlocal与Synchronized区别

ThreadLocalsynchronized 是 Java 中用于处理多线程编程的两种不同机制。它们各自解决的问题和使用场景有所不同。

1.ThreadLocal

ThreadLocal 是一种机制,允许为每个线程维护一个独立的变量副本。这意味着每个线程都可以拥有自己的局部变量,这些变量不会与其他线程共享。

特点:

  • 线程隔离ThreadLocal 为每个线程提供了一个独立的变量副本,因此线程之间不会相互干扰。
  • 安全性:由于每个线程都有自己的变量副本,所以不存在数据竞争问题,不需要担心同步问题。
  • 使用场景ThreadLocal 适用于需要在线程之间隔离数据的情况,例如每个线程需要维护自己的数据库连接、线程局部变量等。

2.synchronized

synchronized 是 Java 中的关键字,用于实现线程之间的同步。它可以用来修饰方法或代码块,确保同一时刻只有一个线程可以执行被 synchronized 修饰的代码。

特点:

  • 互斥性synchronized 确保了同一时刻只有一个线程可以执行被修饰的方法或代码块。
  • 可见性和原子性synchronized 保证了线程之间的可见性和原子性,即一个线程对共享变量的修改对其他线程是可见的,并且操作是不可分割的。
  • 使用场景synchronized 适用于需要多个线程共享数据,并且需要确保数据一致性的情况,例如计数器、共享资源的管理等。

总结

  • ThreadLocal 用于在线程之间隔离数据,每个线程都有独立的变量副本,不需要同步。
  • synchronized 用于保证线程之间的同步,确保共享数据的一致性。

两者的主要区别在于它们解决的问题不同:ThreadLocal 解决的是线程之间的数据隔离问题,而 synchronized 解决的是线程之间的同步问题。在实际应用中,可以根据具体的需求选择合适的方法。

5. 内存溢出和内存泄漏

5.2 内存溢出 (Memory Overflow)

内存溢出是指程序在运行过程中分配的内存超过了JVM所能提供的最大内存限制,导致程序无法正常运行的一种异常情况。在Java中,内存溢出通常发生在以下几个方面:

  1. 堆内存溢出 (Heap Memory Overflow):

    • 当JVM无法为新的对象分配足够的空间时会发生这种情况。这通常是由于不断创建新对象而没有及时释放不再使用的对象,导致垃圾回收机制无法清理足够的内存空间。
    • 常见错误信息为 java.lang.OutOfMemoryError: Java heap space
  2. 栈内存溢出 (Stack Memory Overflow):

    • 发生在递归调用或者深度嵌套调用时,如果递归深度过大或函数调用栈过深,则可能导致栈溢出。
    • 常见错误信息为 java.lang.StackOverflowError
  3. 方法区溢出 (PermGen or Metaspace Overflow):

    • 发生在方法区(在JDK 8之前称为永久代 PermGen space,在JDK 8及以后版本称为元空间 Metaspace)。
    • 这通常是由于加载了过多的类或常量池过大等原因导致的。
    • 常见错误信息为 java.lang.OutOfMemoryError: PermGen spacejava.lang.OutOfMemoryError: Metaspace
  4. 本地方法栈溢出 (Native Method Stack Overflow):

    • 与栈内存溢出类似,但涉及的是本地方法栈。
    • 常见错误信息为 java.lang.OutOfMemoryError: Java native method stack overflow

5.2 内存泄漏 (Memory Leak)

内存泄漏是指程序在申请内存后未能释放已分配的内存,随着时间推移,越来越多的内存被占用但不再被使用,最终可能导致程序崩溃或性能下降。内存泄漏的特点是分配的内存不会被自动回收,即使程序本身还在正常运行。

内存泄漏的表现形式包括但不限于:

  1. 未释放的对象引用:在Java中,如果一个对象不再被任何引用所指向,那么垃圾收集器会自动回收该对象。但如果存在一些无效的引用(例如静态集合中存储的对象引用),那么这些对象将不会被回收,从而导致内存泄漏。

  2. 循环引用:特别是在使用引用计数的垃圾回收机制的语言中,但在Java中,垃圾收集器会处理循环引用,因此这不是一个常见问题。

  3. 缓存管理不当:如果缓存策略不当,可能会导致缓存中存储了大量的不再需要的数据,这些数据占据的内存不会被释放。

  4. 监听器/回调注册:注册了监听器或回调但没有相应的注销操作,会导致监听器或回调对象一直被持有,从而造成内存泄漏。

  5. 日志记录:日志记录可能会保留大量的对象引用,如果没有正确的管理,也可能导致内存泄漏。

  6. 线程局部变量:使用 ThreadLocal 存储对象时,如果没有正确地清理这些对象,也可能会导致内存泄漏。

内存泄漏通常比内存溢出更难以检测和修复,因为它不一定立即导致程序崩溃,但会逐渐消耗内存资源,影响程序性能和稳定性。为了避免内存泄漏,开发人员需要仔细设计程序结构,确保所有分配的资源都能被适时地释放。

6. 强引用、软引用、弱引用和虚引用

在Java中,对象引用分为四种类型:强引用(Strong)、软引用(Soft)、弱引用(Weak)和虚引用(Phantom)。每种引用类型都有其特定的行为和用途,主要用于实现内存管理的不同需求。

6.1 强引用 (Strong Reference)

  • 定义:这是最常用的引用类型。如果一个对象有一个强引用指向它,那么垃圾收集器永远不会回收这个对象。
  • 实现:普通的对象引用就是强引用,例如 Object obj = new Object();
  • 特点:只要强引用存在,对象就不会被回收。

6.2 软引用 (Soft Reference)

  • 定义:软引用是用来描述一些非必需但仍然有用的对象。如果内存足够,软引用指向的对象不会被回收;如果内存不足,那么软引用指向的对象会被回收。
  • 实现:通过 java.lang.ref.SoftReference 类实现。
  • 特点
    • 当 JVM 认为内存不足时,它会回收软引用指向的对象。
    • 一般用于实现内存敏感的缓存。

6.3 弱引用 (Weak Reference)

  • 定义:弱引用比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。(主动调用System.gc();之前)
  • 实现:通过 java.lang.ref.WeakReference 类实现。
  • 特点
    • 在垃圾回收时一定会被回收。
    • 一般用于实现更严格的缓存策略,或者与 ReferenceQueue 结合使用。

6.4 虚引用 (Phantom Reference)

  • 定义:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它相当于没有任何引用,随时都可能被垃圾回收器回收。
  • 实现:通过 java.lang.ref.PhantomReference 类实现。
  • 特点
    • 虚引用必须与 ReferenceQueue 关联。
    • 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象之前,把这个虚引用加入到与之关联的队列中。
    • 一般用于跟踪对象的销毁过程,比如实现对象的 “finalize” 通知机制。

6.5 示例代码

下面是一个简单的示例代码,演示如何使用这四种引用类型:

import java.lang.ref.*;

public class ReferenceDemo {
    private static final int _1MB = 1024 * 1024;

    public static void createReferences() {
        // 强引用
        Object strongRef = new byte[4 * _1MB];

        // 软引用
        SoftReference<Object> softRef = new SoftReference<>(new byte[4 * _1MB]);

        // 弱引用
        WeakReference<Object> weakRef = new WeakReference<>(new byte[4 * _1MB]);

        // 虚引用
        PhantomReference<Object> phantomRef = new PhantomReference<>(new byte[4 * _1MB], new ReferenceQueue<>());
    }

    public static void checkReferences() {
        System.out.println("Checking references...");
        
        // 检查软引用
        if (softRef.get() != null) {
            System.out.println("Soft reference is still valid.");
        } else {
            System.out.println("Soft reference has been cleared.");
        }

        // 检查弱引用
        if (weakRef.get() != null) {
            System.out.println("Weak reference is still valid.");
        } else {
            System.out.println("Weak reference has been cleared.");
        }

        // 检查虚引用
        if (phantomRef.get() == null) {
            System.out.println("Phantom reference has been cleared.");
        } else {
            System.out.println("Phantom reference is still valid.");
        }
    }

    public static void main(String[] args) {
        createReferences();

        // 手动触发垃圾回收
        System.gc();
        try {
            Thread.sleep(1000); // 给垃圾收集器一点时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        checkReferences();
    }
}
  • 强引用:是最常用的引用类型,指向的对象不会被垃圾回收器回收。
  • 软引用:在内存不足时被回收,适用于实现内存敏感的缓存。
  • 弱引用:在下次垃圾回收时被回收,适用于实现更严格的缓存策略。
  • 虚引用:主要用于跟踪对象的销毁过程,需要与 ReferenceQueue 结合使用。

7. Threadlocal原理分析

  1. 在每个线程中都有自己独立的ThreadLocalMap对象,ThreadLocalMap对象底层基于Entry对象封装。
  2. 如果当前线程对应的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对。 Key 为当前new ThreadLocal()对象,value 就是为object变量值。

Set方法源码

一个线程对应一个独立ThreadLocalMap,一个ThreadLocalMap k==ThreadLocal value缓存变量的值

//ThreadLocal类
public void set(T value) {
    //获取到当前的线程
    Thread t = Thread.currentThread();
    //获取到当前线程中的threadLocals   threadLocals类型ThreadLocalMap 底层基于Entry对象封装
    //Entry对象 key=ThreadLocal value缓存的变量的值
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //this就是在调用这个方法之前new 出的 ThreadLocal 对象
        map.set(this, value);
    } else { 
        createMap(t, value);
    }
}
//ThreadLocal类
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// Thread 类中的
ThreadLocal.ThreadLocalMap threadLocals = null;
//ThreadLocal类中的ThreadLocalMap 类
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

Get方法源码

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程对应的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //this就是在调用这个方法之前new 出的 ThreadLocal 对象
        //根据具体threadLocal对象(this)获取value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

image-20240804175926921

8. ThreadLocal 内存泄漏问题及避免方法

1.ThreadLocal 内存泄漏的原因

ThreadLocal 是 Java 中用于在线程内部存储数据的一种机制。每个线程都有一个独立的 ThreadLocalMap,用来存储线程局部变量。ThreadLocalMap 使用 Entry 对象来封装键值对,其中键是 ThreadLocal 实例,而值则是线程局部变量的实际值。

ThreadLocal 作为Entry对象的key,是弱引用,当ThreadLocal指向null的时候,Entry对象中的key变为null,该对象一直无法被垃圾收集机制回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。

2.如何避免 ThreadLocal 内存泄漏

为了避免 ThreadLocal 导致的内存泄漏,可以采取以下几种措施:

  1. 手动调用 remove() 方法

    每个 ThreadLocal 实例都有一个 remove() 方法,可以用来从当前线程的 ThreadLocalMap 中移除该实例对应的条目。当不再需要某个线程局部变量时,应该显式地调用此方法来释放资源。

    myThreadLocal.remove();
    
  2. 在每次 set() 方法调用时清理 null

    ThreadLocalset() 方法会在设置新值之前检查是否有 null 键存在,并将其从 ThreadLocalMap 中移除。因此,频繁地调用 set() 方法可以帮助清理不再使用的条目。

  3. 使用弱引用

    ThreadLocal 的设计已经考虑到了内存泄漏的风险,它默认使用弱引用作为键,这样即使没有显式地调用 remove() 方法,当没有强引用指向 ThreadLocal 实例时,该实例最终也会被垃圾回收。

3.示例代码

下面是使用 ThreadLocal 并避免内存泄漏的一个示例:

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

public class SafeThreadLocal<K> extends ThreadLocal<K> {

    @Override
    protected K initialValue() {
        return null;
    }

    @Override
    public void remove() {
        super.remove();
        // 显式调用 remove 方法以确保从 ThreadLocalMap 中删除该条目
    }

    @Override
    public void set(K value) {
        super.set(value);
        // 每次调用 set 时都会自动清理 null 键
    }

    // 其他方法...

    public static void main(String[] args) {
        SafeThreadLocal<String> threadLocal = new SafeThreadLocal<>();

        // 使用 ThreadLocal
        threadLocal.set("Hello, World!");

        // 不再需要时,显式调用 remove 方法
        threadLocal.remove();
    }
}

// 假设存在一个 ThreadLocalMap 的实现,类似于如下所示
class ThreadLocalMap {
    private final Map<WeakReference<ThreadLocal<?>>, Object> map = new HashMap<>();

    public void put(ThreadLocal<?> key, Object value) {
        map.put(new WeakReference<>(key), value);
    }

    public Object get(ThreadLocal<?> key) {
        return map.get(new WeakReference<>(key));
    }

    public void remove(ThreadLocal<?> key) {
        map.remove(new WeakReference<>(key));
    }

    // 其他方法...
}

通过上述措施,可以有效地避免由 ThreadLocal 引发的内存泄漏问题。

9. ThreadLocal 的引用类型与内存泄漏问题

  • **强引用作为键:**如果 ThreadLocal 的键使用强引用,当我们将 ThreadLocal 的引用设为 null 时,尽管 ThreadLocal 对象本身可能变得不可达,但由于每个线程的 ThreadLocalMap 仍然持有对该对象的强引用,因此 ThreadLocal 对象不会被垃圾回收器回收,这可能会导致内存泄漏。
  • **弱引用作为键:**如果 ThreadLocal 的键使用弱引用,在我们将 ThreadLocal 的引用设为 null 后,如果垃圾回收器运行,那么 ThreadLocal 对象会被回收,其对应的 Entry 的键变为 null。虽然在下一次调用 set 方法时会清理 null 键,但如果在这期间没有调用 set 或者没有机会执行 ThreadLocalMap 的清理逻辑(例如 get 方法中的清理过程),那么含有 null 键的 Entry 将保留在 ThreadLocalMap 中,从而可能导致内存泄漏。
  • **内存泄漏的根本原因:**不论使用强引用还是弱引用,如果不适当地管理 ThreadLocal 的生命周期,都可能发生内存泄漏。最根本的原因在于 ThreadLocalMap 的生命周期通常与线程相同,如果线程持续运行且不清理过期的 ThreadLocal 实例,即使使用了弱引用,也可能导致内存泄漏。
  • **避免内存泄漏的方法:**为了防止内存泄漏,建议在不再需要某个 ThreadLocal 实例时显式调用其 remove 方法,或者确保在适当的时机让垃圾回收器能够回收不再使用的 ThreadLocal 实例。此外,了解 ThreadLocalMap 的清理机制并适时触发清理也是很重要的。

http://www.kler.cn/a/315916.html

相关文章:

  • 软件设计师考试大纲
  • 《Probing the 3D Awareness of Visual Foundation Models》论文解析——多视图一致性
  • Mac解压包安装MongoDB8并设置launchd自启动
  • 力扣 LeetCode 541. 反转字符串II(Day4:字符串)
  • Javascript高级—常见算法
  • 【视觉SLAM】1-概述
  • 数据结构 ——— 常见的时间复杂度计算例题(最终篇)
  • Linux驱动开发 ——架构体系
  • 求最大公约数
  • CSS 布局三大样式简单学习
  • 【解密 Kotlin 扩展函数】命名参数和默认值(十三)
  • 【深入Java枚举类:不仅仅是常量的容器】
  • 数据结构——串的模式匹配算法(BF算法和KMP算法)
  • 设计模式-装饰者模式
  • VMware虚拟机经常性卡死,打开运行一段时间后卡死,CPU占比增至100%
  • 电脑网络怎么弄动态ip :步骤详解与优势探讨
  • Tomcat系列漏洞复现
  • AI时代最好的编程语言应该选择谁?
  • vue h5 蓝牙连接 webBluetooth API
  • MySQL 中删除重复的数据并只保留一条
  • C#实现指南:将文件夹与exe合并为一个exe
  • vscode 环境搭建
  • 神经网络修剪实战
  • ubuntu安装docker compose
  • 解决 TortoiseGitPlink Fatal Error:深入解析
  • JS巧用.padStart()|.padEnd()方法用另一个字符串填充当前字符串