目录
ThreadLocal内存泄漏的原因?
改进和优化
cleanSomeSlots方法
expungeStaleEntry方法
replaceStaleEntry方法
为什么使用弱引用?
Thread.exit()
ThreadLocal内存泄漏最佳解决方案
在使用完毕后立即清理ThreadLocal
使用InheritableThreadLocal替代ThreadLocal
使用弱引用清理ThreadLocal
ThreadLocal是为了解决多线程共享访问对象带来的线程安全问题。它通过为每个线程分配一个对象实例,达到隔离的目的,使得线程之间互不影响。与同步机制不同的是,同步机制以时间换空间,控制线程访问共享对象的顺序,而ThreadLocal则是为每个线程分配一个对象实例,牺牲了空间效率换来时间效率。但是,在ThreadLocal使用过程中存在内存泄漏的风险,如果线程执行结束后,ThreadLocal,ThreadLocalMap,entry都会被回收掉,但在线程池中,线程是复用的,所以ThreadLocal的内存泄漏就值得我们关注。
ThreadLocal内存泄漏的原因主要是因为在使用ThreadLocal时没有及时清理ThreadLocal对象所引用的线程特有的副本。具体来说,当一个线程结束后,如果没有手动清理或者调用remove方法来移除对应的ThreadLocal对象,那么这个ThreadLocal对象仍然会被ThreadLocalMap持有,而ThreadLocalMap是通过弱引用来关联ThreadLocal对象的,如果ThreadLocal对象没有被其他强引用持有,那么在垃圾回收的时候就会被回收,但是对应的线程特有的副本却无法被回收,从而导致内存泄漏。
另外,如果使用线程池来管理线程,线程池中的线程是会被复用的,而不会在每次任务执行结束后销毁线程。这就意味着线程池中的线程仍然持有之前任务中创建的ThreadLocal对象,而这些对象对应的线程特有的副本却不会被释放,从而导致内存泄漏的问题。
对于ThreadLocal内存泄漏的问题,Java在不同版本中进行了不同的改进和优化。以下是一些改进措施:
cleanSomeSlots方法的改进: 在JDK 6之前,ThreadLocalMap中没有自动清理过期Entry的机制。JDK 7引入了cleanSomeSlots方法来解决这个问题。每次调用set或get方法时,会以一定的概率触发该方法,该方法会遍历整个表格,并清理掉过期的Entry。这样可以减轻内存泄漏的风险,使得那些已经过期且无法再被访问的线程特有副本得到释放。
public class MyThreadLocal extends ThreadLocal {
@Override
protected T initialValue() {
// 初始化方法
return ...;
}
@Override
public void set(T value) {
super.set(value);
cleanSomeSlots();
}
@Override
public T get() {
T value = super.get();
cleanSomeSlots();
return value;
}
private void cleanSomeSlots() {
ThreadLocalMap map = getMap(Thread.currentThread());
if (map != null) {
map.cleanSomeSlots();
}
}
}
expungeStaleEntry方法的改进: JDK 8引入了expungeStaleEntry方法,该方法用于显式地清理过期的Entry。在ThreadLocalMap的size超过阈值时被调用,该方法会遍历整个表格,将key为null的Entry移除以释放关联的线程特有副本。
public class MyThreadLocal extends ThreadLocal {
@Override
protected T initialValue() {
// 初始化方法
return ...;
}
@Override
public void set(T value) {
super.set(value);
expungeStaleEntry();
}
@Override
public T get() {
T value = super.get();
expungeStaleEntry();
return value;
}
private void expungeStaleEntry() {
ThreadLocalMap map = getMap(Thread.currentThread());
if (map != null) {
map.expungeStaleEntry();
}
}
}
replaceStaleEntry方法的改进: JDK 9引入了replaceStaleEntry方法,用于在创建新的Entry时替换已经过期的Entry。该方法主要解决了JDK 8中可能出现的并发问题,保证在替换Entry时不会有其他线程同时访问旧的Entry,从而避免了可能的内存泄漏。
public class MyThreadLocal extends ThreadLocal {
@Override
protected T initialValue() {
// 初始化方法
return ...;
}
@Override
public void set(T value) {
super.set(value);
replaceStaleEntry();
}
@Override
public T get() {
T value = super.get();
replaceStaleEntry();
return value;
}
private void replaceStaleEntry() {
ThreadLocalMap map = getMap(Thread.currentThread());
if (map != null) {
map.replaceStaleEntry();
}
}
}
使用弱引用主要是为了解决ThreadLocal中的内存泄漏问题。在线程局部变量中,如果使用强引用,即使在业务代码中将ThreadLocal实例设置为null,由于Entry强引用着ThreadLocal,ThreadLocal对象无法被垃圾回收,从而导致内存泄漏。
而使用弱引用修饰ThreadLocal可以解决这个问题。当ThreadLocal实例不再被业务代码使用时,由于ThreadLocalMap中使用了弱引用来引用ThreadLocal实例,ThreadLocal实例会在下一次垃圾回收时被正确地回收掉。同时,在ThreadLocal的生命周期中会对key为null的脏entry进行处理,避免出现潜在的内存泄漏。
尽管使用弱引用会导致可能出现一些内存泄漏问题,但相比起使用强引用造成的内存泄漏,弱引用的使用能够保证在ThreadLocal的生命周期内尽可能地避免内存泄漏问题,从而提高应用的安全性和可靠性。
需要注意的是,虽然使用弱引用可以减少内存泄漏的潜在问题,但仍然需要在使用ThreadLocal时注意及时清理和移除不再使用的ThreadLocal实例,以确保整体系统的资源利用效率。
Thread.exit()方法是一个废弃的方法,不推荐使用。它会导致线程突然终止,可能会破坏线程的稳定性和数据完整性,并且无法保证所有资源的正确释放。在正常情况下,应该通过执行完任务或者正常结束的方式让线程退出。如果需要强制终止线程,可以通过调用Thread的interrupt方法来进行管理和控制。
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 执行线程的任务
// ...
// 检查中断标志
if (Thread.currentThread().isInterrupted()) {
System.out.println("线程被中断,退出循环");
break;
}
}
System.out.println("线程退出");
});
thread.start();
// 给线程发送中断信号
thread.interrupt();
}
}
在这个示例中,线程在while循环中执行任务,并在每次循环开始时检查中断标志。如果中断标志被设置,线程会退出循环并输出相应信息。
在main方法中,我们使用thread.interrupt()方法给线程发送中断信号。这会将线程的中断标志设置为true。线程在下一次循环开始时会检查到这个中断标志,并做出相应的处理来退出循环。
这种方式可以安全地控制线程的退出,避免了Thread.exit()方法可能导致的问题。同时,它也提供了更灵活和可控的方式来管理线程的生命周期。
由于ThreadLocal为每个线程维护一个独立的变量副本,因此如果没有及时清理ThreadLocal,可能会导致内存泄漏问题。下面是一些解决ThreadLocal内存泄漏问题的最佳实践:
及时清理是防止内存泄漏的最佳解决方案之一。确保在使用完ThreadLocal后调用其remove()方法,清除数据。
特别是在使用线程池的情况下,由于线程的复用性,如果没有清理ThreadLocal,可能会导致线程中保存的数据对后续线程产生干扰,进而导致业务逻辑出现问题。因此,类似于加锁与解锁一样,使用完ThreadLocal后就应该立即清理,以确保下次使用时不会受到上次使用遗留下来的数据的影响。
public class UserContext {
private static final ThreadLocal USER_THREAD_LOCAL = new ThreadLocal<>();
public static void setUser(User user) {
USER_THREAD_LOCAL.set(user);
}
public static User getUser() {
return USER_THREAD_LOCAL.get();
}
public static void clear() {
USER_THREAD_LOCAL.remove();
}
}
在这个示例中,我们定义了一个静态的ThreadLocal变量USER_THREAD_LOCAL,并提供了setUser、getUser和clear方法,在使用完USER_THREAD_LOCAL后,可以调用clear方法清理ThreadLocal。
通过及时清理ThreadLocal,可以有效避免内存泄漏问题,并确保数据在不同线程间的隔离性。
如果需要在父线程和子线程之间共享ThreadLocal变量,可以使用InheritableThreadLocal替代ThreadLocal。InheritableThreadLocal也是一种ThreadLocal,但它可以让子线程继承父线程的ThreadLocal变量副本,从而避免重复创建副本的问题。
public class InheritableRequestContext {
private static final InheritableThreadLocal REQUEST_ID = new InheritableThreadLocal<>();
public static void setRequestId(String requestId) {
REQUEST_ID.set(requestId);
}
public static String getRequestId() {
return REQUEST_ID.get();
}
public static void clear() {
REQUEST_ID.remove();
}
}
在这个示例中,我们使用了InheritableThreadLocal来定义共享变量REQUEST_ID,并提供了setRequestId、getRequestId和clear方法,以便在线程间共享该变量。
使用弱引用来清理ThreadLocal。通过将ThreadLocal变量存储在WeakReference中,可以让垃圾回收器在需要释放内存时自动清理ThreadLocal变量。
public class WeakRequestContext {
private static final ThreadLocal> REQUEST_ID = new ThreadLocal<>();
public static void setRequestId(String requestId) {
REQUEST_ID.set(new WeakReference<>(requestId));
}
public static String getRequestId() {
WeakReference ref = REQUEST_ID.get();
return ref != null ? ref.get() : null;
}
public static void clear() {
REQUEST_ID.remove();
}
}
在这个示例中,我们使用了ThreadLocal和WeakReference来定义变量REQUEST_ID,并提供了setRequestId、getRequestId和clear方法。
总之,为避免ThreadLocal内存泄漏问题,可以采用立即清理、使用InheritableThreadLocal和使用弱引用等多种解决方案。在具体场景中,可以根据实际情况选择最佳的解决方案。