面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)
在java的多线程模块中,ThreadLocal是经常被提问到的一个知识点,提问的方式有很多种,可能是循序渐进也可能是就像我的题目那样,因此只有理解透彻了,不管怎么问,都能游刃有余。
这篇文章主要从以下几个角度来分析理解
1、ThreadLocal是什么
2、ThreadLocal怎么用
3、ThreadLocal源码分析
4、ThreadLocal内存泄漏问题
下面我们带着这些问题,一点一点揭开ThreadLocal的面纱。若有不正之处请多多谅解,并欢迎批评指正。
一、ThreadLocal是什么
从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
现在相信你已经对ThreadLocal有一个大致的认识了,下面我们看看如何用?
二、ThreadLocal怎么用
既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:
public class ThreadLocalTest01 {
    public static void main(String[] args) {
        //新建一个ThreadLocal
        ThreadLocal<String> local = new ThreadLocal<>();
        //新建一个随机数类
        Random random = new Random();
        //使用java8的Stream新建5个线程
        IntStream.range(0, 5).forEach(a-> new Thread(()-> {
            //为每一个线程设置相应的local值
            local.set(a+"  "+random.nextInt(10));
            System.out.println("线程和local值分别是  "+ local.get());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start());
    }
}
/*线程和local值分别是  0  6
  线程和local值分别是  1  4
  线程和local值分别是  2  3
  线程和local值分别是  4  9
  线程和local值分别是  3  5 */
从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。
这就是TheadLocal的基本使用,是不是非常的简单。那么为什么会在数据库连接的时候使用的比较多呢?
class ConnectionManager {
    private static Connection connect = null;
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}
上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。
以上主要是讲解了一个基本的案例,然后还分析了为什么在数据库连接的时候会使用ThreadLocal。下面我们从源码的角度来分析一下,ThreadLocal的工作原理。
三、ThreadLocal源码分析
在最开始的例子中,只给出了两个方法也就是get和set方法,其实还有几个需要我们注意。
//获取ThreadLocal的值
public T get() { }
//设置ThreadLocal的值
public void set(T value) { }
//删除ThreadLocal
public void remove() { }
//初始化ThreadLocal的值
protected T initialValue() { }
方法这么多,我们主要来看set,然后就能认识到整体的ThreadLocal了:
1、set方法
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
OK,到这一步了,相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
static class ThreadLocalMap {
   /**
    * The entries in this hash map extend WeakReference, using
    * its main ref field as the key (which is always a
    * ThreadLocal object).  Note that null keys (i.e. entry.get()
    * == null) mean that the key is no longer referenced, so the
    * entry can be expunged from table.  Such entries are referred to
    * as "stale entries" in the code that follows.
    */
    static class Entry extends WeakReference<ThreadLocal<?>> {
       /** The value associated with this ThreadLocal. */
       Object value;
       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
    }
    //还有很多很多
}
我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
还有一个getMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。
2、get方法
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
通过上面ThreadLocal的介绍相信你对这个方法能够很好的理解了,首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。
如何设置一个初始值呢?
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
原理很简单
3、remove方法
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
从我们的map移除即可。
OK,其实内部源码很简单,现在我们总结一波
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
OK,现在从源码的角度上不知道你能理解不,对于ThreadLocal来说关键就是内部的ThreadLocalMap。
四、ThreadLocal其他几个注意的点
只要是介绍ThreadLocal的文章都会帮大家认识一个点,那就是内存泄漏问题。我们先来看下面这张图。

上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
1、Thread中有一个map,就是ThreadLocalMap
2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
链接:https://zhuanlan.zhihu.com/p/56214714
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
前言
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。但是如果滥用 ThreadLocal,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析 ThreadLocal 内存泄漏的问题
ThreadLocal实现原理ThreadLocal为什么会内存泄漏ThreadLocal最佳实践
ThreadLocal 实现原理

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key是 ThreadLocal 实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key的,弱引用的对象在 GC 时会被回收。
ThreadLocal为什么会内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用
static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。 - 分配使用了
ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。 
为什么使用弱引用
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
我们先来看看官方文档的说法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的
ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。 - key 使用弱引用:引用的
ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。 
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal 最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
- 每次使用完
ThreadLocal,都调用它的remove()方法,清除数据。 
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)的更多相关文章
- 面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》
		
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 说到底,你真的会造火箭吗? 常说面试造火箭,入职拧螺丝.但你真的有造火箭的本事吗,大 ...
 - java面试官:兄弟简单谈谈Static、final、Static final各种用法吧
		
前言 对Static.final.Static final这几个关键词熟悉又陌生?想说却又不知怎么准确说出口?好的,本篇博客文章将简短概要出他们之间的各自的使用,希望各位要是被你的面试官问到了,也能从 ...
 - 面试官问我:谈谈对Java GC的了解?回答完让我回家等消息....
		
JVM的运行数据区 首先我简单来画一张 JVM的结构原理图,如下. 我们重点关注 JVM在运行时的数据区,你可以看到在程序运行时,大致有5个部分. 1.方法区 不止是存“方法”,而是存储整个 clas ...
 - 面试官:ThreadLocal的应用场景和注意事项有哪些?
		
前言 ThreadLocal主要有如下2个作用 保证线程安全 在线程级别传递变量 保证线程安全 最近一个小伙伴把项目中封装的日期工具类用在多线程环境下居然出了问题,来看看怎么回事吧 日期转换的一个工具 ...
 - 面试官就是要问我SpringMVC的源码,差点顶不住!
		
<对线面试官>系列目前已经连载22篇啦!有深度风趣的系列! [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 & ...
 - 【对线面试官】CountDownLatch和CyclicBarrier的区别
		
<对线面试官>系列目前已经连载31篇啦,这是一个讲人话面试系列 [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 &am ...
 - 【对线面试官】Kafka基础入门
		
<对线面试官>系列目前已经连载33篇啦,这是一个讲人话面试系列 [对线面试官]Java注解 [对线面试官]Java泛型 [对线面试官] Java NIO [对线面试官]Java反射 &am ...
 - 你所不知道的 CSS 阴影技巧与细节  滚动视差?CSS 不在话下  神奇的选择器 :focus-within  当角色转换为面试官之后  NPOI 教程 - 3.2 打印相关设置  前端XSS相关整理  委托入门案例
		
你所不知道的 CSS 阴影技巧与细节 关于 CSS 阴影,之前已经有写过一篇,box-shadow 与 filter:drop-shadow 详解及奇技淫巧,介绍了一些关于 box-shadow ...
 - 面试官:能解释一下javascript中bind、apply和call这三个函数的用法吗
		
一.前言 不知道大家还记不记得前几篇的文章:<面试官:能解释一下javascript中的this吗> 那今天这篇文章虽然是介绍javascript中bind.apply和call函数 ...
 - 面试官:"谈谈分库分表吧?"
		
原文链接:面试官:"谈谈分库分表吧?" 面试官:“有并发的经验没?” 应聘者:“有一点.” 面试官:“那你们为了处理并发,做了哪些优化?” 应聘者:“前后端分离啊,限流啊 ...
 
随机推荐
- php 利用 fsockopen GET/POST 提交表单及上传文件
			
1.GET get.php <?php$host = 'demo.fdipzone.com';$port = 80;$errno = '';$errstr = '';$timeout = 30; ...
 - Vue2.0源码学习(4) - 合并配置
			
合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...
 - 解决UIWebView内存不释放问题
			
走访很多朋友,查阅了很多资料发现UIWebView这尼玛就是个坑,有人说是sdk自带的bug....... 所以一个新的方法诞生了#import <WebKit/WebKit.h> WKW ...
 - 华为模拟器在三层交换机上实现dhcp的配置
			
<Huawei>sysEnter system view, return user view with Ctrl+Z.[Huawei]sys sw1[sw1]dhcp enable Inf ...
 - 国内商业智能软件的天下来了,选择BI工具我们应该看这3点!
			
数据变革时代,经常有"外行人"问到,何谓商业智能BI? BI可以说是通过软件或者服务,将网络时代中海量的数据转化为行动上的洞察力,从而影响企业的战略和战术决策. 当海量.高增长率 ...
 - C#内联函数 特性  MethodImplOptions.AggressiveInlining)
			
[MethodImpl(MethodImplOptions.AggressiveInlining)] 内联函数 Impl:implement的缩写 内联函数 在计算机科学中,内联函数(有时称作在线函数 ...
 - parquet列存储本身自带压缩 配合snappy或者lzo等可以进行二次压缩
			
上传txt文件到hdfs,txt文件大小是74左右. 这里提醒一下,是不是说parquet加lzo可以把数据压缩到这个地步,因为我的测试数据存在大量重复.所以下面使用parquet和lzo的压缩效果特 ...
 - redis主从复制,哨兵以及集群搭建部署
			
redis主从复制 1.redis支持多实例的功能,一台机器上,可以运行多个单个的redis数据库 环境准备,运行3个redis数据库,达到 1主 2从的配置 主库 6379.conf port 63 ...
 - Qt:QUrl构造时的qrc前缀
			
参考(按对我帮助从大到小排列): Qt内的各种路径(让人迷惑) - 鬼谷子com - 博客园 qt webengineview 加载本地资源方式 - beautifulday - 博客园 (17条消息 ...
 - Qt:QUrl
			
1.说明 概述 一个代表URL的类,此外还支持国际域名(IDNs). 通常在初始化时传入QString构造QUrl,除此之外还能用setUrl(). URL有两种表示格式:编码.未编码.未编码URL常 ...