ThreadLocal 的总结思考

栏目: Java · 发布时间: 6年前

内容简介:ThreadLocal 的总结思考

ThreadLocal应该是 java 程序员面试常被问到的一个语言特性问题。问的最多的可能就是,ThreadLocal的底层数据结构是什么?

下面的回答基本上可以忽悠面试官了:

1、ThreadLocal底层的数据模型是ThreadLocalMap,该Map是一个基于开放地址法实现的哈希表,key是当前的threadlocal对象,value是当前线程放在这个threadlocal里面的值;

2、 每个线程持有一个ThreadLocalMap对象A,A里面放的是和当前线程相关的threadlocal

3、如果认真看过源码,还会知道ThreadLocalMap的实现和WeakHashMap实现有异曲同工之妙,都通过WeakReference封装了key值,防止可怕的内存泄漏;

4、再举个get()数据的例子。第一步得到当前线程;第二步获取当前线程对应的ThreadLocalMap;第三步以当前threadlocal对象为key,去ThreadLocalMap中把值捞出来;

ThreadLocal的内存模型看起来是下面这个图的样子

ThreadLocal 的总结思考

这个图给人的感觉很别扭,自然也就会产生下面的疑惑:

1、为什么不用个全局的map来实现threadlocal,key为当前threalocal+thread,value为其对应的值。毕竟这个实现简单又易于理解;

2、为什么ThreadLocalMap要用WeakReference来封装key,然后整个代码实现多了一大堆乱七八糟的事情;

3、为什么要用开放地址法实现哈希表,而不用类似于HashMap的链地址法;

我们从一个示例来分析上面的问题

假设用一个全局map来实现threadlocal,大概的代码可能是下面这样的:

ThreadLocal 的总结思考

这个山寨的threadlocal也可以像jdk提供的threadlocal一样使用:

ThreadLocal 的总结思考

功能上看起来是完全满足要求了,但是也带来下面的问题:

放在这个全局map的变量由谁来清理,什么时候清理?

我能想到两个办法:

1、每个ShanzhaiThreadLocal对象在无用的时候把自己从map中移除掉。可是这样做,是要java程序员回到C的时代么,手动管理内存?而且如果程序抛异常了怎么办,catch所有东西,在finally中remove map么?这样代码会不会太丑陋了?

2、在线程死掉的时候从map中移除相关key-value,毕竟我们的key是用thread+threadlocal计算出来的,这个办法可以一把将所有线程相关的缓存内容都干掉!确实很方便,可是万一线程意外终止呢,在什么时候移除?或者线程不断被复用,就像线程池那样,之前也许已经创建了很多ShanzhaiThreadLocal,它们即使已经无用了,但是依然无法被清理,内存又泄漏了!看起来第二个办法还没第一个靠谱……

看来两个办法都不靠谱,谁有更好的办法请告诉我~~

下面看看jdk的ThreadLocal是怎么解决这个问题的:

1、Thread类中,有个变量"ThreadLocal.ThreadLocalMap threadLocals = null",ThreadLocalMap就是存放该thread实际数据的地方,这样做解决了上面说的一个问题,线程死掉后,该thread相关的所有threadlocal数据都被清空了,不管线程是不是意外终止,而且是默默的清理掉,不用应用代码操心(毕竟ThreadLocalMap存储的是和当前thread相关的数据,所以做为它的一个独立变量很合理对不对~~)

2、假设当前thread一直活着(比如赖在线程池中),有些无用的threadlocal对象怎么清理呢?这个就要看接下来WeakReference的用法了~

下面是ThreadLocalMap的部分源码:

ThreadLocal 的总结思考

1、这个map的底层数据结构是一个数组,数组是个key-value对象Entry;

2、Entry对象的key用WeakReference包装了一下;

WeakReference有以下特性:当一个对象A只有WeakReference引用指向它是,那么A在下一次gc的时候就会被回收掉。想象下,如果ThreadLocalMap中某个key已经不用了,最终只会有一个WeakReference指向它,这个key自然就可以被回收掉,不会一直停留在ThreadLocalMap中。

key回收掉了,value值还在啊,这个怎么回收!!!

ThreadLocal的get和set方法每次调用时,如果发现当前的entry的key为null(也就是被回收掉了),最终会调用expungeStaleEntry(int staleSlot)方法,该方法会把哈希表当前位置的无用数据清理掉(当然还有别的操作)。不用担心这个动作在遥远的未来才发生,因为只要get,set数据,那么一点点大的哈希表,总是不难产生哈希冲突的,这时候无用的数据就很容易被发现了!

从上面的源码分析可以看出,假设某个threadlocal对象无效,这个对象本身会在下次gc被回收,对应的value值也会在某次ThreadLocal调用中被释放;如果某个thread死掉了,它对应的threadlocal内容自动释放。

那为什么要用开放地址法实现hash冲突呢?

下面说下我个人的理解:

1、链地址法的本质是在哈希表的同一个位置上存储多个元素,那么我们便将要存储到这一位置的多个元素串成链表,并存储链表头。这个方法简单明了,但是效率方面可能要打折扣,如果碰巧诸君不走运,一组元素全哈希到了一个位置,全被放到一个链表中,查找的效率必然可怜(O(n));而且链地址法还需要存储指针,必然浪费一部分内存;

2、开放地址法:所有的元素都被存储在哈希表中,没有链表,没有存储在哈希表之外的元素;更少的内存,更高的内存利用率——开放地址意味着填满哈希表,另外完全逃离了指针,意味着节省了一笔空间。

但是使用开放地址法,在hash冲突很严重的情况下,效率依然很差!

ThreadLocal中使用0x61c88647这个魔法数据来解决hash冲突的问题,所有的threadlocal对象共享同一个计数器,该计数器每次递增0x61c88647,该数字是一个斐波那契散列乘数,它的优点是通过它散列出来的结果分布会比较均匀,可以很大程度上避免hash冲突。而且ThreadLocalMap需要经常清理无用的对象,使用纯数组形式更方便。

0x61c88647的测试可以参考http://jerrypeng.me/2013/06/thread-local-and-magical-0x61c88647/

所以为什么使用开放地址法,个人总结有以下的考虑因素:

1) 节省内存空间,链表使用的空间大于数组;

2) ThreadLocal设计的哈希key可以尽可能避免哈希冲突;

3) 清理数据效率高,毕竟遍历数组比遍历链表效率高;

使用WeakReference封装key,这样不在使用的ThreadLocal对象可以很快被检查出来并清理掉

使用全局hashmap保存threadlocal对象,需要应用程序手动清理数据才能确保不会有内存泄露,在发生异常等特殊情况下无法保证这点


以上所述就是小编给大家介绍的《ThreadLocal 的总结思考》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

U一点·料

U一点·料

阿里巴巴集团 1688用户体验设计部 / 机械工业出版社 / 2015-7-13 / 79.00元

《U一点·料——阿里巴巴1688UED体验设计践行之路》是1688UED团队历经多年实践之后的心血之作,书中以“道─术─器”的思路为编排脉络,从设计观、思考体系、方法论上层层剖析,将零散的行业knowhow串成体系,对“UED如何发展突破”提出了自己的真知灼见。该书重实战、讲方法、求专业、论文化,是一部走心的诚意之作。 本书作者从美工到用户体验设计师,从感性随意到理性思考,从简单的PS做图到......一起来看看 《U一点·料》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具