欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


欢迎跳转到本文的原文链接:https://honeypps.com/java/thread-local-analysis/

对于ThreadLocal感兴趣是从一个问题开始的:ThreadLocal在何种情况下会发生内存泄露?对于这个问题的思考不得不去了解ThreadLocal本身的实现以及一些细节问题等。接下去依次介绍ThreadLocal的功能,实现细节,使用场景以及一些使用建议。

##概述
ThreadLocal不是用来解决对象共享访问问题的,而主要提供了线程保持对象的方法和避免参数传递的方便的对象访问方式。一般情况下,通过ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

ThreadLocal使用场合主要解决多线程中数据因并发产生不一致的问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来的线程消耗,也介绍了线程并发控制的复杂度。

另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map(Thread类中的ThreadLocal.ThreadLocalMap的变量)中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
【代码1】

    /* 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;

很多人会有这样的无解:感觉这个ThreadLocal对象建立了一个类似于全局的map,然后每个线程作为map的key来存取对应的线程本地的value。其实是ThreadLocal类中有一个ThreadLocalMap静态内部类,可以简单的理解为一个map,这个map为每个线程复制一个变量的“拷贝”存储其中。下面是ThreadLocalMap的部分源码:
【代码2】

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}}     private static final int INITIAL_CAPACITY = 16;private Entry[] table;private int size = 0;private int threshold; // Default to 0//部分省略
}

ThreadLocal类中一共有4个方法:

  • T get()
  • protected T initialValue()
  • void remove()
  • void set(T value)

就以get()方法为例
【代码3】

    public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}return setInitialValue();}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

get()方法的源码如上所示,可以看到map中真正的key是线程ThreadLocal实例本身(ThreadLocalMap.Entry e = map.getEntry(this);中的this)。可以看一下getEntry(ThreadLocal key)的源码.
【代码4】

        private Entry getEntry(ThreadLocal key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}

那么map中的value是什么呢?我们继续来看源码:
【代码5】

    private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}protected T initialValue() {return null;}

代码5中只能够观察到通过[protected T initialValue()]方法设置了一个初始值,当然也可以通过set方法来赋值,继续看源码:
【代码6】

    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

ThreadLocal设置值有两种方案:1. Override其initialValue方法;2. 通过set设置。

关于重写initialValue方法可以参考下面这个例子简便的实现:
【代码7】

    private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){@Overrideprotected Long initialValue(){return System.currentTimeMillis();}};

##内存泄露
通过代码1和代码2的片段可以看出,在Thread类中保有ThreadLocal.ThreadLocalMap的引用,即在一个Java线程栈中指向了堆内存中的一个ThreadLocal.ThreadLocalMap的对象,此对象中保存了若干个Entry,每个Entry的key(ThreadLocal实例)是弱引用,value是强引用(这点类似于WeakHashMap)。

用到弱引用的只是key,每个key都弱引用指向threadLocal,当把threadLocal实例置为null以后,没有任何强引用指向threadLocal实例,所以threadLocal将会被gc回收,但是value却不能被回收,因为其还存在于ThreadLocal.ThreadLocalMap的对象的Entry之中。只有当前Thread结束之后,所有与当前线程有关的资源才会被GC回收。所以,如果在线程池中使用ThreadLocal,由于线程会复用,而又没有显示的调用remove的话的确是会有可能发生内存泄露的问题。

其实在ThreadLocal.ThreadLocalMap的get或者set方法中会探测其中的key是否被回收(调用expungeStaleEntry方法),然后将其value设置为null,这个功能几乎和WeakHashMap中的expungeStaleEntries()方法一样。因此value在key被gc后可能还会存活一段时间,但最终也会被回收,但是若不再调用get或者set方法时,那么这个value就在线程存活期间无法被释放。
【代码8】

        private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// expunge entry at staleSlottab[staleSlot].value = null;tab[staleSlot] = null;size--;// Rehash until we encounter nullEntry e;int i;for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal k = e.get();if (k == null) {e.value = null;tab[i] = null;size--;} else {int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

其实ThreadLocal本身可以看成是没有内存泄露问题的,通过显示的调用remove方法即可。

##使用场景及方式
ThreadLocal的应用场景,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,比如定义一个static变量,同步访问,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

在多线程的开发中,经常会考虑到的策略是对一些需要公开访问的属性通过设置同步的方式来访问。这样每次能保证只有一个线程访问它,不会有冲突。但是这样做的结果会使得性能和对高并发的支持不够。在某些情况下,如果我们不一定非要对一个变量共享不可,而是给每个线程一个这样的资源副本,让他们可以独立都各自跑各自的,这样不是可以大幅度的提高并行度和性能了吗?

还有的情况是有的数据本身不是线程安全的,或者说它只能被一个线程使用,不能被其它线程同时使用。如果等一个线程使用完了再给另一个线程使用就根本不现实。这样的情况下,我们也可以考虑用ThreadLocal。

ThreadLocal建议:

  1. ThreadLocal类变量因为本身定位为要被多个线程来访问,它通常被定义为static变量。
  2. 能够通过值传递的参数,不要通过ThreadLocal存储,以免造成ThreadLocal的滥用。
  3. 在线程池的情况下,在ThreadLocal业务周期处理完成时,最好显示的调用remove()方法,清空“线程局部变量”中的值。
  4. 在正常情况下使用ThreadLocal不会造成OOM, 弱引用的知识ThreadLocal,保存值依然是强引用,如果ThreadLocal依然被其他对象应用,线程局部变量将无法回收。

##InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子类,代码量很少,可以看一下:
【代码9】

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

这里主要的还是一个childValue这个方法。
在代码7中示范了ThreadLocal的方法,而使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。可以采用重写childValue(Object parentValue)方法来更改继承的值。
查看案例:
【代码10】

public class InheriableThreadLocal
{public static final InheritableThreadLocal<?> itl = new InheritableThreadLocal<Object>(){@Override protected Object initialValue(){return new Date().getTime();}@Override protected Object childValue(Object parentValue){return parentValue+" which plus in subThread.";}};public static void main(String[] args){System.out.println("Main: get value = "+itl.get());Thread a = new Thread(new Runnable(){@Override public void run(){System.out.println(Thread.currentThread().getName()+": get value = "+itl.get());}});a.start();}
}

运行结果:

Main: get value = 1467100984858
Thread-0: get value = 1467100984858 which plus in subThread.

如果去掉@Override protected Object childValue(Object parentValue)方法运行结果:

Main: get value = 1461585396073
Thread-0: get value = 1461585396073

参考资料

  1. Java多线程知识小抄集(一)
  2. 深入JDK源码之ThreadLocal类
  3. Java集合框架:WeakHashMap

欢迎跳转到本文的原文链接:https://honeypps.com/java/thread-local-analysis/

欢迎支持笔者新作:《深入理解Kafka:核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


聊一聊ThreadLocal相关推荐

  1. 聊一聊ThreadLocal内存泄漏的问题

    回答任何一个问题的时候应该要遵循:明确题意-->深入浅出-->举例说明-->总结,这四个步骤很重要,可以让你沉着冷静,思路清晰,避免尴尬. 01 -  明确题意 明确题意的意思就是先 ...

  2. 获取返回值作为变量_解决多线程间共享变量线程安全问题的大杀器——ThreadLocal...

    微信公众号:Zhongger 我是Zhongger,一个在互联网行业摸鱼写代码的打工人! 关注我,了解更多你不知道的[Java后端]打工技巧.职场经验等- 上一期,讲到了关于线程死锁.用户进程.用户线 ...

  3. Java并发编程之ThreadLocal源码分析

    1 一句话概括ThreadLocal   什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本. 2 ThreadLocal使用场景   用一句话总结 ...

  4. 聊一聊Android的消息机制

    2019独角兽企业重金招聘Python工程师标准>>> 聊一聊Android的消息机制 侯 亮 1概述 在Android平台上,主要用到两种通信机制,即Binder机制和消息机制,前 ...

  5. 解决多线程间共享变量线程安全问题的大杀器——ThreadLocal

    上一期,讲到了关于线程死锁.用户进程.用户线程的相关知识,不记得的小伙伴可以看看:字节跳动面试官问我:你知道线程死锁吗?用户线程.守护线程的概念与区别了解吗? 这期,我们来聊一聊一个在Java并发编程 ...

  6. 聊一聊Spring中的线程安全性

    原文出处:SylvanasSun Spring与线程安全 ThreadLocal ThreadLocal中的内存泄漏 参考文献 Spring与线程安全 Spring作为一个IOC/DI容器,帮助我们管 ...

  7. FastThreadLocal吞吐量居然是ThreadLocal的3倍

    目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误区),很多文章关于FastThreadLocal介绍的也不全,希望本篇文章可以带你彻底理解FastThreadLoc ...

  8. 正确理解ThreadLocal

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt107 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的, ...

  9. 什么是ThreadLocal

    顾名思义它是local variable(线程局部变量).它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.从线 ...

最新文章

  1. 3.程序的局部性原理
  2. DCMTK:父元素处理的测试程序
  3. 大数据组件需要额外添加的依赖包汇总(持续更新中)
  4. 3 Convex functions
  5. 业余长跑爱好者最后膝盖都怎样了?
  6. B+树 范围查询_为什么 MySQL 使用 B+ 树,而不是 B 树或者 Hash?
  7. 【软考】2021软件设计师复习开坑指南
  8. Win10之选择文件右击时卡死问题(转圈圈)
  9. 【react】 redux 公共状态管理---数据的渲染,数据的修改,再把修改的数据渲染到当前组件...
  10. shell 逐行读取文件
  11. Atitit s2018 s3 doc list alldvc.docx .docx s2018 s3f doc compc s2018 s3f doc homepc sum doc dvcCom
  12. 小米手机抢购背后的摩尔定律
  13. msg1500说明书_瑞斯康达MSG1500 路由 刷机 保姆级教程
  14. 系统可靠性分析与设计
  15. 今年春节北京烟花爆竹备货量下降46.7%
  16. start()和run()方法的区别
  17. Linux FTP 21端口始终无法连接的问题
  18. 带你使用JS-SDK自定义微信分享效果
  19. 我的第一篇论文诞生的故事
  20. Python数据挖掘:利用聚类算法进行航空公司客户价值分析

热门文章

  1. sketchup作品_18级园林工程技术专业课程实训作品展
  2. bzoj2442codevs4654 单调队列优化dp
  3. struts2重定向
  4. 成功更新至Win8.1 update
  5. 【Asp.Net】Asp.Net CommandName作用
  6. Daily scrum[2013.12.07]
  7. 换光纤猫 ZXA10 F420
  8. CodeForces - 670C Cinema(离散化+排序/map,水题)
  9. (转)快速统计二进制中1的个数
  10. UVA - 1606 Amphiphilic Carbon Molecules