【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区别
ThreadLocal
和 synchronized
是 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中,内存溢出通常发生在以下几个方面:
-
堆内存溢出 (
Heap Memory Overflow
):- 当JVM无法为新的对象分配足够的空间时会发生这种情况。这通常是由于不断创建新对象而没有及时释放不再使用的对象,导致垃圾回收机制无法清理足够的内存空间。
- 常见错误信息为
java.lang.OutOfMemoryError: Java heap space
。
-
栈内存溢出 (
Stack Memory Overflow
):- 发生在递归调用或者深度嵌套调用时,如果递归深度过大或函数调用栈过深,则可能导致栈溢出。
- 常见错误信息为
java.lang.StackOverflowError
。
-
方法区溢出 (
PermGen or Metaspace Overflow
):- 发生在方法区(在JDK 8之前称为永久代 PermGen space,在JDK 8及以后版本称为元空间 Metaspace)。
- 这通常是由于加载了过多的类或常量池过大等原因导致的。
- 常见错误信息为
java.lang.OutOfMemoryError: PermGen space
或java.lang.OutOfMemoryError: Metaspace
。
-
本地方法栈溢出 (
Native Method Stack Overflow
):- 与栈内存溢出类似,但涉及的是本地方法栈。
- 常见错误信息为
java.lang.OutOfMemoryError: Java native method stack overflow
。
5.2 内存泄漏 (Memory Leak)
内存泄漏是指程序在申请内存后未能释放已分配的内存,随着时间推移,越来越多的内存被占用但不再被使用,最终可能导致程序崩溃或性能下降。内存泄漏的特点是分配的内存不会被自动回收,即使程序本身还在正常运行。
内存泄漏的表现形式包括但不限于:
-
未释放的对象引用:在Java中,如果一个对象不再被任何引用所指向,那么垃圾收集器会自动回收该对象。但如果存在一些无效的引用(例如静态集合中存储的对象引用),那么这些对象将不会被回收,从而导致内存泄漏。
-
循环引用:特别是在使用引用计数的垃圾回收机制的语言中,但在Java中,垃圾收集器会处理循环引用,因此这不是一个常见问题。
-
缓存管理不当:如果缓存策略不当,可能会导致缓存中存储了大量的不再需要的数据,这些数据占据的内存不会被释放。
-
监听器/回调注册:注册了监听器或回调但没有相应的注销操作,会导致监听器或回调对象一直被持有,从而造成内存泄漏。
-
日志记录:日志记录可能会保留大量的对象引用,如果没有正确的管理,也可能导致内存泄漏。
-
线程局部变量:使用
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原理分析
- 在每个线程中都有自己独立的
ThreadLocalMap
对象,ThreadLocalMap
对象底层基于Entry对象封装。 - 如果当前线程对应的
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();
}
8. ThreadLocal 内存泄漏问题及避免方法
1.ThreadLocal 内存泄漏的原因
ThreadLocal
是 Java 中用于在线程内部存储数据的一种机制。每个线程都有一个独立的 ThreadLocalMap
,用来存储线程局部变量。ThreadLocalMap
使用 Entry
对象来封装键值对,其中键是 ThreadLocal
实例,而值则是线程局部变量的实际值。
ThreadLocal
作为Entry
对象的key
,是弱引用,当ThreadLocal
指向null
的时候,Entry
对象中的key
变为null
,该对象一直无法被垃圾收集机制回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。
2.如何避免 ThreadLocal 内存泄漏
为了避免 ThreadLocal
导致的内存泄漏,可以采取以下几种措施:
-
手动调用
remove()
方法每个
ThreadLocal
实例都有一个remove()
方法,可以用来从当前线程的ThreadLocalMap
中移除该实例对应的条目。当不再需要某个线程局部变量时,应该显式地调用此方法来释放资源。myThreadLocal.remove();
-
在每次
set()
方法调用时清理null
键ThreadLocal
的set()
方法会在设置新值之前检查是否有null
键存在,并将其从ThreadLocalMap
中移除。因此,频繁地调用set()
方法可以帮助清理不再使用的条目。 -
使用弱引用
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
的清理机制并适时触发清理也是很重要的。