在面试的时候,ThreadLocal作为高并发常用工具经常会被问到。而面试官比较喜欢问的问题有以下两个:

1、ThreadLocal是怎么实现来保证每个线程的变量副本的。

2、ThreadLocal的内存泄露是怎么产生的,怎么避免内存泄露。

首先我们来看第一个问题,实际上这个问题主要是想考察候选人是否有阅读过ThreadLocal的源码。当然阅读源码前我们得了解ThreadLocal是怎么使用的,只有使用过ThreadLocal我们才能产生对ThreadLocal是怎么做到的这样的疑问。而带着问题去阅读源码才能有一种廓然开朗的感觉。废话不多说,放码过来。

贴上threadlocal官方示例

public class ThreadId {     // Atomic integer containing the next thread ID to be assigned     private static final AtomicInteger nextId = new AtomicInteger(0);     // Thread local variable containing each thread's ID     private static final ThreadLocal threadId =         new ThreadLocal() {             @Override              protected Integer initialValue() {                 return nextId.getAndIncrement();             }     };      // Returns the current thread's unique ID, assigning it if necessary     public static int get() {         return threadId.get();     } }

上面代码主要通过从threadlocal得到该线程的id,如果当前线程没有的时候则生成一个。

通过上述例子我们会有如下疑问:

1、为什么ThreadLocal可以获取到当前线程的变量副本?

-> 猜测ThreadLocal内部有一个Map对象(Map),key为线程对象,value为我们存储的变量。

带着上述疑问,我们看下ThreadLocal的源码实现。

先看set方法。

public class ThreadLocal {    public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);    }       ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }        void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }    }

从getMap方法得知,ThreadLocal内部确实存在一个Map(ThreadLocalMap),只不过该map是线程的一个内部属性。而从createMap方法我们得知ThreadLocalMap是以ThreadLocal作为key,我们提供的值为value。

进入到Thread类源码查看,有两个ThreatLocalMap类型的属性。从注释可以看出,第一个是由ThreadLocal类维护的,第二个是由InheritableThreadLocal类维护的。而ThreadLocalMap是ThreadLocal的静态内部类。

public class Thread implements Runnable {  /* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;    /*     * InheritableThreadLocal values pertaining to this thread. This map is     * maintained by the InheritableThreadLocal class.     */    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;  ......}

ThreadLocalMap内部维护的是一个Entry数组,而Entry数组继承WeakReference。我们知道,weakReference是当jvm内存不足时会回收只有WeakReference引用的对象。而ThreadLocalMap这样做的意图是为什么呢?这个问题会跟第二个疑问一起解答。

static class ThreadLocalMap {   static class Entry extends WeakReference<ThreadLocal>> {            /** The value associated with this ThreadLocal. */            Object value;            Entry(ThreadLocal> k, Object v) {                super(k);                value = v;            }        }        private Entry[] table;        ......}

如果是第一次看ThreadLocal源码的话,看到这里可能觉得有点绕,画个图理清一下关系。

首先创建了一个ThreadLocal对象

在一个线程里调用ThreadLocal的get方法,假设是第一次调用得到了null,这时我们在通过数据库查询得到value并调用set方法设置了进去,如果该Thread的threadlocals属性没有被初始化过则会执行ThreadLocal的createMap方法。

下次该线程执行调用get方法时就会从得到先set进去的值。

上面没有贴出get方法的代码,但是我们可以猜测出是通Thread.currentThread().threadLocals.get(this).value获取到值的。这里就不贴出来了。


第一个疑问解决了,现在看下面试问到的第二个问题。ThreadLocal的内存泄露是怎么产生的,怎么避免内存泄露?

网上搜ThreadLocal内存泄很多文章都会说到ThreadLocal的ThreadLocalMap的Entry数组继承的是WeakReference,而WeakReference会在jvm内存不足是回收引用。当thread常驻或者使用线程池时核心线程常驻thread未回收,而ThreadLocal被回收,但是value又是强引用,因此不会被回收而存在内存泄露。当我看了网上形形色色的文章都是这样的描述后仍然云里雾里,而且也不符合我们实际的使用场景。

产生这个问题主要有以下几个方面的原因:

一方面是我们虽然使用过ThreadLocal,但是对ThreadLocal会导致的内存泄露问题场景不熟悉导致的。

另一方面是我们虽然没有正确使用ThreadLocal,但是内存泄露问题并不足于导致OOM(或者说存在内存泄露的问题,但是并不致命)。

以下分几个场景讨论ThreadLocal内存泄露的问题。

场景一:一个请求对应一个新线程

这种场景下使用ThreadLocal,Thread里的threatLocals变量会随着线程的销毁而销毁,自然也就不存在内存泄露的问题了。

场景二:存在强引用的几个ThreadLocal + 少量核心线程数的线程池

使用ThreadLocal的场景中,我们多数都是定义为static类变量,但是并不是意味着只有static修饰才是强引用,只要有被别的类进行强引用的都算,只是定义为static类变量是我们比较常用的场景(参照官方例子)。

由于ThreadLocal存在外部强引用,因此ThreadLocalMap的key不会出现null的情况,而少量核心线程数意味着变量副本不会很多。因此内存泄露的量为

s = coreThreadNums * (objectSize1 + objectSize2 + ... + objectSizeX), X = threadLocalNums

因此当线程池核心线程数不多,threadLocal数量不多和threadLocal变量副本不大时,虽然存在内存泄露,但是却不足于导致OOM。但是当threadLocal变量副本比较大时内存泄露的情况就会严重,导致OOM的可能性加剧了。

场景三:无强引用的几个ThreadLocal + 少量核心线程数的线程池

解析跟场景二类似,只是threadlocalmap的key会变为null,但是value却不会被回收。因此还是存在内存泄露的情况。

场景二和场景三还有一个前提条件是线程池运行的task对threadlocal的使用是一次性的。因为当调用threadLocal的set、get和remove方法时会将threadlocalmap里key为null的entry的value释放掉(实际调用的是ThreadLocalMap的expungeStaleEntry方法,感兴趣的去看源码)。当然使用了强引用的ThreadLocal是享受不到该好处的。

场景四:无强引用的大量ThreadLocal少量核心线程数的线程池

跟场景三类似,只是threadLocal作为threadlocal的key占用的内存稍微大了一点,

场景五:存在强引用的少量ThreadLocal大量核心线程数的线程池

解析跟场景二类型,由于每个常驻线程都有副本,因此内存泄露的情况加剧了。假设1000个核心线程,每个变量副本大小为1m,2个threadlocal则导致2g的内存泄露。

场景六:无强引用的少量ThreadLocal + 大量核心线程数的线程池

跟场景四类似。

场景七:无强引用ThreadLocal + 大量常驻线程的线程池服务于特定任务

这里的特定任务是指任务里都会用到threadlocal get或set 方法的任务,由于每次都调用了get或set方法,threadlocalmap会清理掉key为null的,但是当没有任务执行时,threadlocalmap的value仍然不会被回收,存在内存泄露。

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadLocalTests {    public static void main(String[] args) throws Exception{        final ThreadLocal threadLocal1 = new ThreadLocal<>();        final ThreadLocal threadLocal2 = new ThreadLocal<>();        final ExecutorService threadPool = Executors.newFixedThreadPool(1000);        Thread.sleep(10000L);        for (int i = 0; i < 1000; i++){            threadPool.submit(() -> {                byte[] bytes = new byte[1024 * 1024];                threadLocal1.set(bytes);            });            threadPool.submit(() -> {                byte[] bytes = new byte[1024 * 1024];                threadLocal2.set(bytes);            });        }    }}

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ThreadLocalTests {    public static void main(String[] args) throws Exception{        final ThreadLocal threadLocal1 = new ThreadLocal<>();        final ThreadLocal threadLocal2 = new ThreadLocal<>();        final ExecutorService threadPool = Executors.newFixedThreadPool(1000);        Thread.sleep(10000L);        for (int i = 0; i < 1000; i++){            threadPool.submit(() -> {                byte[] bytes = new byte[1024 * 1024];                threadLocal1.set(bytes);                threadLocal1.remove();            });            threadPool.submit(() -> {                byte[] bytes = new byte[1024 * 1024];                threadLocal2.set(bytes);                threadLocal2.remove();            });        }    }}

private void remove(ThreadLocal> key) {            Entry[] tab = table;            int len = tab.length;            int i = key.threadLocalHashCode & (len-1);            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                if (e.get() == key) {                    e.clear();                    expungeStaleEntry(i);                    return;                }            }}
public void clear() {     this.referent = null;}

其他场景就不讨论了,可自行扩展。

这里做个总结。

无论threadlocal数量多少和常驻线程数量的多少都会导致内存泄露的问题,只是严重程度不同罢了。

那怎样才是使用ThreadLocal的正确姿势而不会导致内存泄露呢?这里举例zuul的requestContext例子。

zuul的requestContext

public class RequestContext extends ConcurrentHashMap<String, Object> {    protected static final ThreadLocal extends RequestContext> threadLocal = new ThreadLocal() {        @Override        protected RequestContext initialValue() {            try {                return contextClass.newInstance();            } catch (Throwable e) {                throw new RuntimeException(e);            }        }    };        /**     * unsets the threadLocal context. Done at the end of the request.     */    public void unset() {        threadLocal.remove();    }}
public class ZuulServlet extends HttpServlet {  @Override    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {        try {            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);            // Marks this request as having passed through the "Zuul engine", as opposed to servlets            // explicitly bound in web.xml, for which requests will not have the same data attached            RequestContext context = RequestContext.getCurrentContext();            context.setZuulEngineRan();            try {                preRoute();            } catch (ZuulException e) {                error(e);                postRoute();                return;            }            try {                route();            } catch (ZuulException e) {                error(e);                postRoute();                return;            }            try {                postRoute();            } catch (ZuulException e) {                error(e);                return;            }        } catch (Throwable e) {            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));        } finally {            // 实际执行threadlocal的清理方法            RequestContext.getCurrentContext().unset();        }    }}

只要在每次用完threadlocal后执行threadlocal的remove方法就可以清除掉变量副本,这样就不会产生内存泄露了。

ThreadLocalMap的remove方法,调用ThreadLocal的remove方法实际上是执行了ThreadLocalMap的remove方法。

private void remove(ThreadLocal> key) {            Entry[] tab = table;            int len = tab.length;            int i = key.threadLocalHashCode & (len-1);            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                if (e.get() == key) {                    e.clear();                    expungeStaleEntry(i);                    return;                }            }        }

inputstreamreader未关闭会导致oom_ThreadLocal 一定会导致内存泄露?相关推荐

  1. inputstreamreader未关闭会导致oom_Linux内核OOM机制分析和防止进程被OOM杀死的方法...

    问题描述 Linux 内核有个机制叫 OOM killer(Out-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽而内核会把该进程 ...

  2. inputstreamreader未关闭会导致oom_【搞定面试官】你还在用Executors来创建线程池?会有什么问题呢?

    前言 上文我们介绍了JDK中的线程池框架Executor.我们知道,只要需要创建线程的情况下,即使是在单线程模式下,我们也要尽量使用Executor.即: ExecutorService fixedT ...

  3. 记录由于未关闭加速器就关机而导致的再次启动后DNS配置错误

    记录由于未关闭加速器就关机而导致的再次启动后DNS配置错误 先直接给出解决方案: cmd输入 netsh winsock reset 这段时间在同学的推荐下入坑了鹅鹅鸭(Goose Goose Duc ...

  4. 【vagrant虚拟机扩容】 vagrant扩容硬盘时克隆失败--未关闭虚拟机导致

    vagrant导入镜像后,空间不足. 扩容硬盘时在克隆硬盘阶段出现以下2种看不太懂的错误 PS D:\MyEnvironment\VirtualBoxVMs\cdh2> vboxmanage c ...

  5. Do not use “@ts-ignore“ because it alters compilation errors(ts严格模式未关闭导致的项目启动失败的解决方案)

    ts严格模式未关闭导致的项目启动失败解决方案 项目场景: 问题描述: 原因分析: 解决方案: 项目场景: 用ts搭建vue项目的时候用@ts-ignore来避免报错,结果编译时仍然出错导致无法启动服务 ...

  6. 数据库连接未关闭,导致数据库拒绝连接问题

    关于数据库连接未关闭,超过150条执行的问题,超过后数据库回拒绝监听

  7. 加载文件流_未关闭的文件流会引起内存泄露么?

    专注于Java领域优质技术,欢迎关注 来自:技术小黑屋 最近接触了一些面试者,在面试过程中有涉及到内存泄露的问题,其中有不少人回答说,如果文件打开后,没有关闭会导致内存泄露.当被继续追问,为什么会导致 ...

  8. 遇到一个因socket未关闭引发的文件句柄用完问题

    "爱提踢斯"项目最近遇到一个问题,当FTP服务器磁盘没有空间时,设备会不断复位--这是测试人员反馈的.我们拿到log后,看到一个通信所用的文件打开失败.不断打印Too many o ...

  9. Windows 10 无法连接网络:未关闭科学上网软件、连接科学上网软件后,关闭科学上网软件无法上网、设置默认不开启代理服务器

    1.关闭电脑时未关闭科学上网软件 问题描述 这个也不算是问题,只是在关闭电脑时,如果没有先关闭科学上网软件,可能网络还是走代理,但是科学上网软件又没有开,所以代理和原始的网络都走不通,导致无法上网. ...

最新文章

  1. 【微信】微信小程序 微信开发工具 创建js文件报错 pages/module/module.js 出现脚本错误或者未正确调用 Page()...
  2. 001_Servlet简介
  3. 用完成例程(Completion Routine)实现的重叠I/O模型
  4. 布局 线宽 间距 走线 泪滴 过孔 【快速提升PCB板Layout质量的6个细节】
  5. python螺旋圆的绘制_python 使用turtule绘制递归图形(螺旋、二叉树、谢尔宾斯基三角形)...
  6. 使用svm 对参数寻优的时候出现错误
  7. 【python】Django设置SESSION超时时间没有生效?
  8. Python之Numpy操作基础
  9. Flask最强攻略 - 跟DragonFire学Flask - 第十六篇 Flask-Migrate
  10. java解析project mpp文件,如何在Java中创建.mpp文件?
  11. 电影《海贼王:红发歌姬》观后感
  12. oracle中的||是什么意思?
  13. 利用python批量查询企业信息_用Python批量查询域名(并行化,附源代码)
  14. jadx工具windows下载
  15. git clone出现 fatal: unable to access ‘https://github.com/...‘的解决办法(亲测有效)
  16. 怎么写安卓手机脚本_拉结尔手游攻略,云手机全自动挂机刷副本及装备
  17. 我爱机器学习网机器学习类别文章汇总
  18. OpenGL显示窗口重定形函数
  19. 什么软件运用计算机处理图像,平面设计中计算机图形图像处理软件的运用探究...
  20. 家里WiFi信号差,如何解决?

热门文章

  1. mysql悲观锁关键字_MySQL悲观锁 select for update实现秒杀案例(jfinal框架)
  2. 《零基础》MySQL 事务(二十二)
  3. linux php oci,Linux下PHP连接Oracle数据库
  4. 3 配置ftp文件服务器,03-FTP和TFTP配置
  5. php 解析yaml,php yaml 解析 报错问题
  6. java 一个月的第一天_java中如何正确获得一个月的第一天和最后一天
  7. 【OpenCV 例程200篇】97. 反谐波平均滤波器
  8. 从一个点云里面创建一个深度图
  9. object detection错误之no module named nets
  10. opencv5-图像混合