ThreadLocal必知必会
前言
自从被各大互联网公司的"造火箭"级面试难度吊打之后,痛定思痛,遂收拾心神,从基础的知识点开始展开地毯式学习。每一个非天才程序猿都有一个对35岁的恐惧,而消除恐惧最好的方式就是面对它、看清它、乃至跨过它,学习就是这个世界给普通人提供的一把成长型武器,掌握了它,便能与粗暴的生活一战。
最近看了好几篇有关ThreadLocal的面试题和技术博客,下面结合源码自己做一个总结,以方便后面的自我回顾。
本文重点:
1、ThreadLocal如何发挥作用的?
2、ThreadLocal设计的巧妙之处
3、ThreadLocal内存泄露问题
4、如何让新线程继承原线程的ThreadLocal?
下面开始正文。
一、ThreadLocal如何发挥作用的?
首先来一段本地demo,工作中用的时候也是类似的套路,先声明一个ThreadLocal,然后调用它的set方法将特定对象存入,不过用完之后一定别忘了加remove,此处是一个错误的示范...
public class ThreadLocalDemo { private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args) {
threadLocal.set("main thread");
new Thread(() -> {
threadLocal.set("thread");
}).start();
}
}
追踪一下set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 1、得到map
if (map != null)
map.set(this, value); // 2、放入value
else
createMap(t, value); // 3、初始化map
}
在threadLocal的set方法中有三个主要方法,第一个方法是去当前线程的threadLocals中获取map,该map是Thread类的一个成员变量。
如果线程是新建出来的,threadLocals这个值肯定是null,此时会进入方法3 createMap中(如下)新建一个ThreadLocalMap,存入当前的ThreadLocal对象和value。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
相对而言最复杂的是方法2 map.set()方法,如下,该方法代码位于ThreadLocal的内部类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); // 1、获取要存放的key的数组下标 for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) { ///2、如果下标所在位置是空的,则直接跳过此for循环,不为空则进入内部判断逻辑,否则往下移动数组指针 ***
ThreadLocal<?> k = e.get();
// 2.1 如果不是空,则判断key是不是原数组下标处Entry对象的key,是的话直接替换value即可
if (k == key) {
e.value = value;
return;
}
// 2.2 如果数组下标处的Entry的key是null,说明弱引用已经被回收,此时也替换掉value ***
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 3、说明数组中i所在位置是空的,直接new一个Entry赋值
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 4、清理掉一些无用的数据 ***
rehash();
}
该方法加了注释,重要的地方均用 *** 标识了出来,虽然可能无法清楚每一步的用意与原理,但大体做了什么都能知道---在此方法中完成了value对象的存储。
写到这里的时候,BZ的思维也不清晰了,赶紧画个图清醒下:
完成set操作后,当前线程、threadLocal变量、ThreadLocal对象、ThreadLocalMap之间的关系基本梳理出来了。
插播一个扩展,补充一下引用相关的知识。Java中的强引用是除非代码主动修改或者持有引用的变量被清理,否则该引用指向的对象一定不会被垃圾回收器回收;软引用是只要JVM内存空间够用,就不会对该引用指向的对象进行垃圾回收;而弱引用是只要进行垃圾回收时该对象只有弱引用,则就会被回收。
Entry类的弱引用实现如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
下面开始填坑。
二、ThreadLocal设计的巧妙之处
上面ThreadLocalMap.set方法的代码中,标识了三颗星的第二步有什么意义?
答:找到第一个未被占的下标位置。ThreadLocalMap中的Entry[]数组是一个环状结构,通过nextIndex方法即可证明,当i+1比len大的时候,返回0即初始位置。当出现hash冲突时,HashMap是通过在下标位置串接链表来存放数据,而ThreadLocalMap不会有那么大的访问量,所以采用了更加轻便的解决hash冲突的方式-往后移一个位置,看看是不是空的,不是空的则继续往后移,直到找到空的位置。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
为什么编写JDK代码的大佬们要将Entry的key设置为弱引用?标识了三颗星的2.2步为什么key会是null?
答:key设置为弱引用是为了当threadLocal被清理之后堆中的ThreadLocal对象也能被清理掉,避免ThreadLocal对象带来的内存泄露。这也是key是null的原因-当只有key这个弱引用指向ThreadLocal对象时,发生一次垃圾回收就会将该ThreadLocal回收了。但这种方式没法完全避免内存泄露,因为回看之前的内存分布图,key指向的对象虽然被释放了内存,但是value还在啊,而且由于这个value对应的key是null,也就不会有地方使用这个value,完蛋,内存释放不了了。
这时2.2的逻辑就发挥一部分作用了,如果当前i下标的key是null,说明已经被回收了,那么直接把这个位置占用就行了,反正已经没人用了。
标识了三颗星的第四步 cleanSomeSlots方法的职责是什么?
答:该方法用于清除部分key为null的Entry对象。为什么是清除部分呢?且看方法实现:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
在do/while循环中,每次循环给n右移一位(传入的n是数组中存放的数据个数),如果遇到一个key为null的情况, 说明数组中可能存在多个这种对象,所以将n置为整个数组的长度,多循环几次,并且调用了expungeStaleEntry方法将key为null的value引用去掉。cleanSomeSlots方法没有采用完全循环遍历的方式,主要出于方法执行效率的考量。
下面再详细说说expungeStaleEntry方法的逻辑,该方法专门用于清除key为null的这种过期数据,而且还附带一个作用:将之前因为hash冲突导致下标后移的对象收缩紧凑一些,提高遍历查询效率。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 1、清除入参所在下标的value
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 2、从入参下标开始往后遍历,一直遍历到tab[i]等于null的位置停止
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // 2.1 如果key为null,找的就是这种浑水摸鱼的,必除之而后快
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) { // 2.2 h即当前这个entry的key应该在的下标位置,如果跟i不同,说明这个entry是发生下标冲突后移过来的
tab[i] = null; // 此时要将现在处于i位置的e移到h位置,故先将tab[i]置为null,在后面再将tab[i]位置的e存入h位置 // Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null) // 2.3 这里通过while循环来找到h以及后面第一个为null的下标位置,这个位置就是存放e的位置
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
为什么存放线程相关的变量要这样设计?为何不能在ThreadLocal中定义一个Map的成员变量,key就是线程,value就是要存放的对象,这样设计岂不是更简洁易懂?
答:这样设计能做到访问效率和空间占用的最优。先看访问效率,如果采用平常思维的方式用一个公共Map来存放key-value,则当多线程访问的时候肯定会有访问冲突,即使使用ConcurrentHashMap也同样会有锁竞争带来的性能消耗,而现在这种将map存入Thread中的设计,则保证了一个线程只能访问自己的map,并且是单线程肯定不会有线程安全问题,简直不要太爽。
三、ThreadLocal内存泄露问题
文章开头的示例中,用static修饰了ThreadLocal,这样做是否必要?有什么作用?
答:用static修饰ThreadLocal变量,使得在整个线程执行过程中,Map中的key不会被回收(因为有一个静态变量的强引用在引用着呢),所以想什么时候取就什么时候取,而且从头到尾都是同一个threadLocal变量(再new一个除外),存入map中时也只占用一个下标位置,不会出现不可控的内存占用超限。由此可见,设置为static并不是完全必要,但作用是有的。
ThreadLocal中针对key为null的情况,在好几处用不同的姿势进行清除,就是为了避免内存泄漏,这样是否能完全避免内存泄漏?若不能,如何做才能完全避免?
答:能最大程度的避免内存泄漏,但不能完全避免。线程执行完了就会将ThreadLocalMap内存释放,但如果是线程池中的线程,一直重复利用,那么它的Map中的value数据就可能越攒越多得不到释放引起内存泄露。如何避免?用完后在finally中调一下remove方法吧,前辈大佬们都给写好了的方法,且用即可。
另外,threadLocal变量不能是局部变量,因为key是弱引用,如果设置成局部变量,则方法执行完之后强引用清除只剩弱引用,就可能被释放掉,key变为null,这样也就背离了ThreadLocal在同一个线程经过多个方法时共享同一个变量的设计初衷。
四、如何让新线程继承原线程的ThreadLocal?
答:new一个InheritableThreadLocal对象set数据即可,这时会存入当前Thread的成员变量 inheritableThreadLocals中。当在当前线程中new一个新线程时,在新线程的init方法中会将当前线程的inheritableThreadLocals存入新线程中,完成数据的继承。
Old Thread(ZZQ):毕生功力都传授给你了,还不赶紧去为祸人间?
New Thread(Pipe River): ...
ThreadLocal必知必会的更多相关文章
- 读书笔记汇总 - SQL必知必会(第4版)
本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...
- 读书笔记--SQL必知必会--建立练习环境
书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL in 10 Minutes - Fourth Edition> MyS ...
- 读书笔记--SQL必知必会12--联结表
12.1 联结 联结(join),利用SQL的SELECT在数据查询的执行中联结表. 12.1.1 关系表 关系数据库中,关系表的设计是把信息分解成多个表,一类数据一个表,各表通过某些共同的值互相关联 ...
- 读书笔记--SQL必知必会18--视图
读书笔记--SQL必知必会18--视图 18.1 视图 视图是虚拟的表,只包含使用时动态检索数据的查询. 也就是说作为视图,它不包含任何列和数据,包含的是一个查询. 18.1.1 为什么使用视图 重用 ...
- 《MySQL 必知必会》读书总结
这是 <MySQL 必知必会> 的读书总结.也是自己整理的常用操作的参考手册. 使用 MySQL 连接到 MySQL shell>mysql -u root -p Enter pas ...
- 《SQL必知必会》学习笔记(一)
这两天看了<SQL必知必会>第四版这本书,并照着书上做了不少实验,也对以前的概念有得新的认识,也发现以前自己有得地方理解错了.我采用的数据库是SQL Server2012.数据库中有一张比 ...
- SQL 必知必会
本文介绍基本的 SQL 语句,包括查询.过滤.排序.分组.联结.视图.插入数据.创建操纵表等.入门系列,不足颇多,望诸君指点. 注意本文某些例子只能在特定的DBMS中实现(有的已标明,有的未标明),不 ...
- .NET程序员项目开发必知必会—Dev环境中的集成测试用例执行时上下文环境检查(实战)
Microsoft.NET 解决方案,项目开发必知必会. 从这篇文章开始我将分享一系列我认为在实际工作中很有必要的一些.NET项目开发的核心技术点,所以我称为必知必会.尽管这一系列是使用.NET/C# ...
- 0005 《SQL必知必会》笔记01-SELECT语句
1.SELECT基本语句: SELECT 字段名1,···,字段名n FROM 表名 2.检索所有字段,用"*"替换字段名,这会导致效率低下 SELECT * FROM 表名; 3 ...
- 2015 前端[JS]工程师必知必会
2015 前端[JS]工程师必知必会 本文摘自:http://zhuanlan.zhihu.com/FrontendMagazine/20002850 ,因为好东东西暂时没看懂,所以暂时保留下来,供以 ...
随机推荐
- JVM原理与深度调优(三)
jvm垃圾收集算法 1.引用计数算法每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收.此方法简单,无法解决对象相互循环引用的问题.还有一个问题是如何解决精准计 ...
- Asp.Net Core 3.1学习-依赖注入、服务生命周期(6)
1.前言 面向对象设计(OOD)里有一个重要的思想就是依赖倒置原则(DIP),并由该原则牵引出依赖注入(DI).控制反转(IOC)及其容器等概念.在学习Core依赖注入.服务生命周期之前,下面让我们先 ...
- 从实践出发:微服务布道师告诉你Spring Cloud与Boot他如何选择
背景 随着公司业务量的飞速发展,平台面临的挑战已经远远大于业务,需求量不断增加,技术人员数量增加,面临的复杂度也大大增加.在这个背景下,平台的技术架构也完成了从传统的单体应用到微服务化的演进. 系统架 ...
- 【20180129】java进程经常OOM,扩容swap。
导读:线上一台服务器专门做为公司内部apk打包服务,由于app的业务和功能与时俱增,apk打包需要依赖的资源越来越多,最近这几天每次apk打包的时候都会由于OOM导致打包失败.由于apk打包业务并不是 ...
- Oliver Twist
对于济贫院那些绅士们而言,贫民好吃懒做.贪得无厌.他们消耗的食物即是对教区最大的威胁. 绅士们的利益得不到满足时,孤儿们只能被驱之而后快,甚至被"加价出售". 然而,眼泪这种东西根 ...
- Java_Web--JDBC 增加记录操作模板
如果不能成功链接数据库,我的博客JAVA中有详细的介绍,可以看一下 import java.sql.Connection; import java.sql.DriverManager; import ...
- python(If 判断)
一.if判断 如果 条件满足,才能做某件事情, 如果 条件不满足,就做另外一件事情,或者什么也不做 注意: 代码的缩进为一个 tab 键,或者 4 个空格 在 Python 开发中,Tab 和空格不要 ...
- python selenium(常用关键字)
1.文本按钮操作相关: send_keys()输入文本 from selenium import webdriver import time dr = webdriver.Chrome() dr.ge ...
- Jenkins 源代码管理(SVN)
Subversion 安装插件 1.首先将本地的自动化用例打包上传 svn 2.配置 jenkins 源代码管理(每次执行 jenkins 时,会自动 check-ou t配置地址中的代码到 Jenk ...
- python字符串分段组合(更新)
描述 获得输入的一个字符串s,以字符减号(-)分割s,将其中首尾两段用加号(+)组合后输出. ...