目录

  • ThreadLcoal源码浅析
  • ThreadLocal的垃圾回收
    • Java引用
    • ThreadLocal的回收
    • 各线程中threadLocalMap的回收
    • 内存泄露问题
  • 总结
  • 参考

ThreadLcoal源码浅析

我们知道ThreadLocal用于维护多个线程线程独立的变量副本,这些变量只在线程内共享,可跨方法、类等,如下是一个维护多个线程Integer变量的ThreadLocal:

ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();

每个使用threadLocalNum的线程,可以通过形如threadLocalNum.set(1)的方式创建了一个独立使用的Integer变量副本,那么它是怎么实现的呢?我们今天就来简单的分析一下。

先看下ThreadLocal的set方法是如何实现的,源码如下:

public void set(T value) {Thread t = Thread.currentThread();  //获取当前线程ThreadLocalMap map = getMap(t);     //获取当前线程的ThreadLocalMapif (map != null)map.set(this, value);           //当前线程的ThreadLocalMap不为空则直接设值elsecreateMap(t, value);            //当前线程的ThreadLocalMap为空则创建一个来设置值}

是的,你没有看错,是获取当前线程中的ThreadLocalMap来设置的值,我们来看一下getMap(t)是如何实现的:

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

然后我们看到Thread中包含了一个ThreadLocalMap类型的属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

到这里我们可以得出一个结论:各个线程持有了一个ThreadLocalMap的属性,通过ThreadLocal设置变量时,直接设置到了对应线程的的ThreadLocalMap属性中

那么不同的线程中通过ThreadLocal设置的值是如何关联定义的ThreadLocal变量和Thread中的ThreadLocalMap的呢?我们接着分析。

前面写到当前线程的ThreadLocalMap为空则创建一个ThreadLocalMap来设值,我们来看下createMap(t, value)的具体实现:

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}///
//ThreadLocalMap构造器定义如下
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  //table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}
private static final int INITIAL_CAPACITY = 16;

线程中threadLocals是一个ThreadLocalMap变量,其默认值是null,该线程在首次使用threadLocal对象调用set的时候通过createMap(Thread t, T firstValue)实例化。

先来看一下ThreadLocalMap,它是在ThreadLocal中定义的一个静态内部类,其内属性如下:

        /*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;/*** The number of entries in the table.*/private int size = 0;/*** The next size value at which to resize.*/private int threshold; // Default to 0

其中属性private Entry[] table,用于存储通过threadLocal set 进来的变量,Entry定义如下:

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

Entry继承了WeakReference<ThreadLocal<?>>,ThreadLocal在构造器中被指定为弱引用super(k)(后面会单独讨论为何这里使用弱引用)。

至此,我们可以知道ThreadLocal和Thead的内存结构如下:

ThreadLocal的垃圾回收

网上看到很多文章都在讲ThreadLocal的内存泄露问题,所以也在这里简单说一下自己的理解。

从上面的结构可以看出ThreadLocal涉及到的要回收的对象包括:

  • ThreadLocal实例本身
  • 各线程中的threadLocalMap,其中包括各个Entry的 key, value

下面先简述java的引用,然后分别讨论ThreadLocal本身的回收和threadLcoalMap的回收

Java引用

  • 强引用(StrongReference):对象可达就不会被gc回收,空间不足时报error
  • 软引用(SoftReference):对象无其他强引用,当空间不足时才会被gc回收。
  • 弱引用(WeakReference):对象无其他强引用,gc过程扫描到就会被回收。

ThreadLocal的回收

ThreadLocal实例的引用主要包括两种:

  • ThreadLocal定义处的强引用
  • 各线程中ThreadLocalMap里的key=weak(threadLocal), 是弱引用

强引用还在的情况下ThreadLocal一定不会被回收;无强引用后,由于各个Thread中Entry的key是弱引用,会在下次GC后变为null。ThreadLocal实例什么时候被回收完全取决于强引用何时被干掉,那么什么时候强引用会被销毁呢?最简单的就是 threadLocal=null强引用被赋值为null;其它也可是threadLocal是一个局部变量,在方法退出后引用被销毁,等等。

这里来回答一下前面提到的为什么ThreadLocalMap中将key设计为弱引用,我们假设如果ThreadLocalMap中是强引用会出现什么情况?定义ThreadLocal时定义的强引用被置为null的时候,如果还有其它使用了该ThreadLocal的线程没有完成,还需要很久会执行完成,那么这个线程将一直持有该ThreadLocal实例的引用,直到线程完成,期间ThreadLocal实例都不能被回收,最重要的是如果不了解ThreadLocal内部实现,你可能都不知道还有其他线程引用了threadLocal实例。

线程结束时清除ThreadLocalMap的代码Thread.exit()如下:

   /*** This method is called by the system to give a Thread* a chance to clean up before it actually exits.*/private void exit() {if (group != null) {group.threadTerminated(this);group = null;}/* Aggressively null out all reference fields: see bug 4006245 */target = null;/* Speed the release of some of these resources */threadLocals = null;inheritableThreadLocals = null;inheritedAccessControlContext = null;blocker = null;uncaughtExceptionHandler = null;}

所以,对于threadLocal对象本身而言, 只要通过threadLocal=null就可以实现回收了。

各线程中threadLocalMap的回收

单从引用的角度来看,各线程中的threadLocalMap,其中包括各个Entry的key 和 value。线程(也就是Thread实例)本身一直持有threadLocalMap的强引用,只有在线程结束的时候才会被回收。而key是threadLocal对象的弱引用,当threadLocal被置为null时就会被回收,此时的Entry数组中就会出现很多key为null,但是value有值的元素,那么value在threadLocal对象为空后应该怎么回收呢?

ThreadLocal在实现的时候提供了一些方法:set/get/remove,可以在执行它们的时候调用ThreadLocalMap的方法回收ThreadLocalMap中已经失效(key=null)的entry实例。

这里就以set为例看看ThreadLocal是如何回收entry的,ThreadLocal set方法实现如下:

//ThreadLocal
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);  // 本次要分析的方法elsecreateMap(t, value);   //这里前面已经分析了}//ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {// We don't use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);  //获取当前threadLocal实例的hashcode,同时也是table的下标//这里for循环找key,是因为hash冲突会使hashcode指向的下标不是真实的存储位置for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get();//找到了设置为新值if (k == key) {e.value = value;return;}//entry不为null,key为null//说明原来被赋值过,但是原threadLocal已经被回收if (k == null) {replaceStaleEntry(key, value, i);return;}}//如果下标对应的entry为null, 则新建一个entrytab[i] = new Entry(key, value);int sz = ++size;//清理threadlocal中其它被回收了的entry(也就是key=null的entry)if (!cleanSomeSlots(i, sz) && sz >= threshold)//rehashrehash();}

看一下cleanSomeSlots的实现:

//ThreadLocalMap
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {//获取下一个entry的下标i = nextIndex(i, len);Entry e = tab[i];//entry不为null,key为null//说明原来被赋值过,但是原threadLocal已经被回收if (e != null && e.get() == null) {n = len;removed = true;// 删除已经无效的entryi = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;}private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 回收无效entrytab[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();//entry不为null,key为null,应该回收if (k == null) {e.value = null;tab[i] = null;size--;} else {//rehash的实现//计算当前entry的k的hashcode,看是下标是否应该为i//如果不为i说明,是之前hash冲突放到这儿的,现在需要reashint h = k.threadLocalHashCode & (len - 1);//h!=i 说明hash冲突了, entry不应该放在下标为i的位置if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.//找正确的位置h,但是还是有可能冲突所以要循环while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

从上面的分析我们可以看到把ThreadLocalMap中的key设计为weakReference,也使set方法可以通过key==null && entry != null判断entry是否失效

总结一下ThreadLocal set方法的实现:

  • 根据threadLocal计算hashcode找到entry[]数组对应位置设置值
  • 遍历数组找到其它失效的(entry不为null,key为null)的entry删除

内存泄露问题

ThreadLocal通过巧妙的设计最大程度上减少了内存泄露的可能,但是并没有完全消除。

当我们使用完ThreadLocal后没有调用set/get/remove方法,那么可能会导致失效内存不能及时被回收,导致内存泄露,尤其是在value占用内存较大的情况。

所以最佳实践是,在明确ThreadLocal不再使用时,手动调用remove方法及时清空。

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal是通过让线程内的ThreadLocalMap.Entry的key指向自身,来实现了对线程内对象的引用,从而可以在线程内方便的使用变量。同时因为操作的都是线程内的变量,也避免了实例线程安全的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 cleanSomeSlots 方法回收键为 null 的 Entry 对象的值(即失效实例)从而防止内存泄漏(其它的remove,get类似)
  • 在明确ThreadLocal不再使用时,手动调用remove方法及时清空

参考

正确理解Thread Local的原理与适用场景

转载于:https://www.cnblogs.com/chrischennx/p/9557285.html

ThreadLocal 从源码角度简单分析相关推荐

  1. Mybatis底层原理学习(二):从源码角度分析一次查询操作过程

    在阅读这篇文章之前,建议先阅读一下我之前写的两篇文章,对理解这篇文章很有帮助,特别是Mybatis新手: 写给mybatis小白的入门指南 mybatis底层原理学习(一):SqlSessionFac ...

  2. 跟我学Kafka源码Producer分析

    2019独角兽企业重金招聘Python工程师标准>>> 跟我学Kafka源码Producer分析 博客分类: MQ 本章主要讲解分析Kafka的Producer的业务逻辑,分发逻辑和 ...

  3. MyBatis源码骨架分析

    源码包分析 MyBatis 源码下载地址:https://github.com/MyBatis/MyBatis-3 MyBatis源码导入过程: 下载MyBatis的源码 检查maven的版本,必须是 ...

  4. [Java] HashMap 源码简要分析

    特性 * 允许null作为key/value. * 不保证按照插入的顺序输出.使用hash构造的映射一般来讲是无序的. * 非线程安全. * 内部原理与Hashtable类似. 源码简要分析 publ ...

  5. android view 源码分析,Android ViewPager源码详细分析

    1.问题 由于Android Framework源码很庞大,所以读源码必须带着问题来读!没有问题,创造问题再来读!否则很容易迷失在无数的方法与属性之中,最后无功而返. 那么,关于ViewPager有什 ...

  6. 【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  7. NJ4X源码阅读分析笔记系列(一)——项目整体分析

    NJ4X源码阅读分析笔记系列(一)--项目整体分析 NJ4X是什么 参见NJ4X的官网:http://www.nj4x.com/ Java and .Net interfaces to support ...

  8. LinkedHashMap 源码详细分析(JDK1.8)

    1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...

  9. 推荐 7 个 Vue2、Vue3 源码解密分析的开源项目

    大家好,我是你们的 猫哥,那个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ 1. 为什么要学习源码 ? 阅读优秀的代码的目的是让我们能够写出优秀的代码. 不给自己设限,不要让你周围人的技术上限成为你的上限.其 ...

最新文章

  1. 深度详解ResNet到底在解决一个什么问题?
  2. linux cant open file for writing,linux 安装rz sz lrz lsz sftp: cannot open 文件名称 to write 报错解决...
  3. 第四期直播分享预告-高薪offer指南
  4. 一图理解M0不同优先级中断及Pendsv切换
  5. [转载]网站分析的最基本度量(3)——网站分析工具如何辨别UV
  6. SQL类似sleep延时语句
  7. MySQL主从配置实战笔记
  8. uboot_v2016 版本中fw_printenv的编译问题
  9. PyTorch深度学习(B站刘二大爷)第八讲作业——Kaggle网站泰坦尼克号Titanic
  10. poj1985和poj1849(树的直径)
  11. TCP/IP OSI各层协议,ARP属于哪一层的协议
  12. 程序员笔记软件cherrytree推荐
  13. usart串口发送与接收问题
  14. linux待机唤醒_Linux电源管理-休眠与唤醒
  15. 松勤软件测试试题,如果让你测试一个完全不熟悉的系统,你会怎么办?
  16. 程序员-实用在线小工具
  17. chrome黑暗模式_黑暗模式:如何克服黑暗面
  18. DIV布局——爱影评在线电影(10页面)) 学生动漫网页设计模板下载 大学生HTML网页制作作品 简单漫画网页设计成品 dreamweaver学生网站模板
  19. 个人设计web前端大作业~响应式游戏网站源码(HTML+CSS+Bootstrap)
  20. requests爬虎妞

热门文章

  1. 上传文件到服务器地址怎么配置,文件上传到服务器怎么配置
  2. python 最小二乘回归 高斯核_机器学习技法6-(支持向量回归)
  3. php表单实现分页,**PHP分步表单提交思路(分页表单提交)
  4. 怎么设置html编译报错,doctype html编译出错,提示unexpected character D,这是怎么回事呀?...
  5. C++安全方向(三):3.5 SHA-1 算法分析和代码演示
  6. 通过html备份数据库文件,备份网站文件和数据库
  7. 缩略图修复_如何解决SOLIDWORKS不显示缩略图预览的方法?
  8. html5头像裁剪,移动端 上传头像 并裁剪功能(h5)
  9. jQuery 事件方法(交互)
  10. matplotlib绘制K线图