文档章节

Java ThreadLocal的内存泄漏问题

我爱春天的毛毛雨
 我爱春天的毛毛雨
发布于 2018/11/14 13:02
字数 1624
阅读 14
收藏 2

ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:

- 存储单个线程上下文信息。比如存储id等;

- 使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;

- 减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。

 

原理

ThreadLocal里类型的变量,其实是放入了当前Thread里。每个Thread都有一个{@link Thread#threadLocals},它是一个map:{@link java.lang.ThreadLocal.ThreadLocalMap}。这个map的entry是{@link java.lang.ThreadLocal.ThreadLocalMap.Entry},具体的key和value类型分别是{@link ThreadLocal}和 {@link Object}。(注:实际是ThreadLocal的弱引用WeakReference<ThreadLocal<?>>,但可以先简单理解为ThreadLocal。)

当设置一个ThreadLocal变量时,这个map里就多了一对ThreadLocal -> Object的映射。

通过一个简单程序来说明上图:

package example.concurrency.tl;
 
/**
 * @author liuhaibo on 2018/05/23
 */
public class ThreadLocalDemo {
 
    private static final ThreadLocal<Integer> TL_INT = ThreadLocal.withInitial(() -> 6);
    private static final ThreadLocal<String> TL_STRING = ThreadLocal.withInitial(() -> "Hello, world");
 
    public static void main(String... args) {
        // 6
        System.out.println(TL_INT.get());
        TL_INT.set(TL_INT.get() + 1);
        // 7
        System.out.println(TL_INT.get());
        TL_INT.remove();
        // 会重新初始化该value,6
        System.out.println(TL_INT.get());
    }
}


-------------------------------
 | TL_INT    -> 6 |
 | TL_STRING -> "Hello, world"|

对于一个普通的map,取其中某个key对应的值分两步:

1. 找到这个map;

2. 在map中,给出key,得到value。

 

想取出我们存放在当前线程里的map里的值同样需要这两步。但是,我们不需要告诉jvm map在哪儿,因为jvm知道当前线程,也知道其局部变量map。所以最终的get操作只需要知道key就行了:int localInt = TL_INT.get();。

看起来有些奇怪,不同于常规的map的get操作的接口的样子。

 

为什么key使用弱引用

不妨反过来想想,如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用(即:TL_INT,是一个强引用,指向ThreadLocal@123456)被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

 

那使用弱引用的好处呢?

如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:TL_INT强引用,和ThreadLocalMap中Entry的弱引用。一旦TL_INT被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。

那么问题来了,ThreadLocal@123456对象只是作为ThreadLocalMap的一个key而存在的,现在它被回收了,但是它对应的value并没有被回收,内存泄露依然存在!而且key被删了之后,变成了null,value更是无法被访问到了!针对这一问题,ThreadLocalMap类的设计本身已经有了这一问题的解决方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的时候,会自动清理key为null的value。如此一来,value也能被回收了。

既然对key使用弱引用,能使key自动回收,那为什么不对value使用弱引用?答案显而易见,假设往ThreadLocalMap里存了一个value,gc过后value便消失了,那就无法使用ThreadLocalMap来达到存储全线程变量的效果了。(但是再次访问该key的时候,依然能取到value,此时取得的value是该value的初始值。即在删除之后,如果再次访问,取到null,会重新调用初始化方法。)

 

内存泄露

总结一下内存泄露(本该回收的无用对象没有得到回收)的原因:

1 弱引用一定程度上回收了无用对象,但前提是开发者手动清理掉ThreadLocal对象的强引用(如TL_INT)。只要线程一直不死,ThreadLocalMap的key-value一直在涨。

解决方法:当某个ThreadLocal变量(比如:TL_INT)不再使用时,记得TL_INT.remove(),删除该key。

2 在上例中,ThreadLocalDemo持有static的ThreadLocal类型:TL_INT,导致TL_INT的生命周期跟ThreadLocalDemo类的生命周期一样长。意味着TL_INT不会被回收,弱引用形同虚设,所以当前线程无法通过ThreadLocalMap的防护措施清除TL_INT所对应的value(Integer)的强引用。通常,我们需要保证作为key的TL_INT类型能够被全局访问到,同时也必须保证其为单例,因此,在一个类中将其设为static类型便成为了惯用做法。殊不知这样增加了ThreadLocal的使用风险。

ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

 

线程池

使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。

 

解决方法参考:

/**
 * Method invoked upon completion of execution of the given Runnable.
 * This method is invoked by the thread that executed the task. If
 * non-null, the Throwable is the uncaught {@code RuntimeException}
 * or {@code Error} that caused execution to terminate abruptly.
 *
 * <p>This implementation does nothing, but may be customized in
 * subclasses. Note: To properly nest multiple overridings, subclasses
 * should generally invoke {@code super.afterExecute} at the
 * beginning of this method.
 *
... some deleted ...
 *
 * @param r the runnable that has completed
 * @param t the exception that caused termination, or null if
 * execution completed normally
 */
protected void afterExecute(Runnable r, Throwable t) { }
 
override {@link ThreadPoolExecutor#afterExecute(r, t)}方法,对ThreadLocalMap进行清理,比如:
 
protected void afterExecute(Runnable r, Throwable t) { 
    // you need to set this field via reflection.
    Thread.currentThread().threadLocals = null;
}

© 著作权归作者所有

我爱春天的毛毛雨
粉丝 5
博文 48
码字总数 114206
作品 0
鸡西
私信 提问
纳尼,Java 存在内存泄泄泄泄泄泄漏吗?

01. 怎么回事? 纳尼,Java 不是自动管理内存吗?怎么可能会出现内存泄泄泄泄泄泄漏! Java 最牛逼的一个特性就是垃圾回收机制,不用像 C++ 需要手动管理内存,所以作为 Java 程序员很幸福,...

ityouknow
05/23
0
0
Java ThreadLocal 类的知识点解读

说起 Java 中的 ThreadLocal 类,可能很多安卓开发人员并不是很熟悉,毕竟很少有使用到的地方。但是如果你仔细分析过 Handler 源码的话,就一定见过这个类的出现。而 Handler 机制又是安卓知...

亦枫
2018/10/29
0
0
Java 200+ 面试题补充 ThreadLocal 模块

让我们每天都有进步,老王带你打造最全的 Java 面试清单,认真把一件事做到极致。 本文是前文《Java 最常见的 200+ 面试题》的第一个补充模块。 1.ThreadLocal 是什么? ThreadLocal 是一个本...

王磊的博客
03/08
677
0
JAVA内存的“防水补漏”解决方案

Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用...

loki_lan
2013/04/09
868
6
java.lang.OutOfMemoryError: Java heap space

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 今天我想说说这个有趣的问题,这的确是个让人费解的是error 问题来源: 这个问题一般会出在myecplise 和ecplise ,...

soul_mate
2014/05/03
355
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周五乱弹 —— 葛优理论+1

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @这次装个文艺青年吧 :#今日歌曲推荐# 分享米津玄師的单曲《LOSER》: mv中的舞蹈诡异却又美丽,如此随性怕是难再跳出第二次…… 《LOSER》-...

小小编辑
今天
205
7
nginx学习笔记

中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。 是连接两个独立应用程序或独立系统的软件。 web请求通过中间件可以直接调用操作系统,也可以经过中间件把请求分发到多...

码农实战
今天
5
0
Spring Security 实战干货:玩转自定义登录

1. 前言 前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证...

码农小胖哥
今天
14
0
JAVA 实现雪花算法生成唯一订单号工具类

import lombok.SneakyThrows;import lombok.extern.slf4j.Slf4j;import java.util.Calendar;/** * Default distributed primary key generator. * * <p> * Use snowflake......

huangkejie
昨天
16
0
PhotoShop 色调:RGB/CMYK 颜色模式

一·、 RGB : 三原色:红绿蓝 1.通道:通道中的红绿蓝通道分别对应的是红绿蓝三种原色(RGB)的显示范围 1.差值模式能模拟三种原色叠加之后的效果 2.添加-颜色曲线:调整图像RGB颜色----R色增强...

东方墨天
昨天
11
1

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部