前言

自从被各大互联网公司的"造火箭"级面试难度吊打之后,痛定思痛,遂收拾心神,从基础的知识点开始展开地毯式学习。每一个非天才程序猿都有一个对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必知必会的更多相关文章

  1. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...

  2. 读书笔记--SQL必知必会--建立练习环境

    书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL in 10 Minutes - Fourth Edition> MyS ...

  3. 读书笔记--SQL必知必会12--联结表

    12.1 联结 联结(join),利用SQL的SELECT在数据查询的执行中联结表. 12.1.1 关系表 关系数据库中,关系表的设计是把信息分解成多个表,一类数据一个表,各表通过某些共同的值互相关联 ...

  4. 读书笔记--SQL必知必会18--视图

    读书笔记--SQL必知必会18--视图 18.1 视图 视图是虚拟的表,只包含使用时动态检索数据的查询. 也就是说作为视图,它不包含任何列和数据,包含的是一个查询. 18.1.1 为什么使用视图 重用 ...

  5. 《MySQL 必知必会》读书总结

    这是 <MySQL 必知必会> 的读书总结.也是自己整理的常用操作的参考手册. 使用 MySQL 连接到 MySQL shell>mysql -u root -p Enter pas ...

  6. 《SQL必知必会》学习笔记(一)

    这两天看了<SQL必知必会>第四版这本书,并照着书上做了不少实验,也对以前的概念有得新的认识,也发现以前自己有得地方理解错了.我采用的数据库是SQL Server2012.数据库中有一张比 ...

  7. SQL 必知必会

    本文介绍基本的 SQL 语句,包括查询.过滤.排序.分组.联结.视图.插入数据.创建操纵表等.入门系列,不足颇多,望诸君指点. 注意本文某些例子只能在特定的DBMS中实现(有的已标明,有的未标明),不 ...

  8. .NET程序员项目开发必知必会—Dev环境中的集成测试用例执行时上下文环境检查(实战)

    Microsoft.NET 解决方案,项目开发必知必会. 从这篇文章开始我将分享一系列我认为在实际工作中很有必要的一些.NET项目开发的核心技术点,所以我称为必知必会.尽管这一系列是使用.NET/C# ...

  9. 0005 《SQL必知必会》笔记01-SELECT语句

    1.SELECT基本语句: SELECT 字段名1,···,字段名n FROM 表名 2.检索所有字段,用"*"替换字段名,这会导致效率低下 SELECT * FROM 表名; 3 ...

  10. 2015 前端[JS]工程师必知必会

    2015 前端[JS]工程师必知必会 本文摘自:http://zhuanlan.zhihu.com/FrontendMagazine/20002850 ,因为好东东西暂时没看懂,所以暂时保留下来,供以 ...

随机推荐

  1. 如何高效使用vim

    Vim 是一款文本编辑器,被称为编辑器之神,非常适合在shell 中编辑代码,熟练的使用Vim,可以让你高效的编写代码. Vim 是Vi 的增强版,所有的类Unix 系统,都自带这两个工具,这两个工具 ...

  2. Maven Wrapper简介

    文章目录 简介 Maven Wrapper的结构 下载Maven Wrapper 使用 Maven Wrapper简介 简介 开发java项目少不了要用到maven或者gradle,对比gradle而 ...

  3. 【集群实战】sersync

    1. sersync介绍 sersync功能: 实时同步: sersync组成: sersync==inotify+rsync inotify: 监控某个目录下面"文件/目录"是否 ...

  4. 全网最清晰的ConstraintLayout教程

    ConstraintLayout是AndroidStudio2.2新增的一个功能,那么这个到底是什么呢?首先第一点我们知道传统的安卓开发,页面基本都是XML编写实现,特别在一些复杂的页面上需要嵌套多层 ...

  5. flask学习笔记(二)

    一.视图函数的传参方式 修改前: 目标: 传参方式改成 途径: 通过request获取参数 注意:args并不是地点类型,而是dict的一个子类,如图: immutable意思是不可变 不可变的字典转 ...

  6. 编写C#程序的IDE

    编写C#程序,在Windows平台下,除了昂贵的Visual Studio.NET这个正宗的工具外,你还了解哪些? 听说有个Eclipse,IBM投钱开发的开源工具,有人也做了个for .NET的pl ...

  7. 单源最短路径(3):SPFA 算法

    SPFA(Shortest Path Faster Algorithm)算法,是西南交通大学段凡丁于 1994 年发表的,其在 Bellman-ford 算法的基础上加上一个队列优化,减少了冗余的松弛 ...

  8. 快放弃你的管家软件吧! Part 2 强制删除

    在Windows系统中,难免会遇到用常规方法无法删除文件的情况. 经常有遇到过一些情况? 软件卸载了,有些文件夹就是删不掉,提示被占用,mmp,这时候你肯定想到了360文件粉碎机! mmp,我就删个文 ...

  9. USACO Training Section 1.1 坏掉的项链Broken Necklace

    题目描述 你有一条由N个红色的,白色的,或蓝色的珠子组成的项链(3<=N<=350),珠子是随意安排的. 这里是 n=29 的二个例子: 第一和第二个珠子在图片中已经被作记号. 图片 A ...

  10. Linux下swap到底有没有必要使用

    周五看到QQ群里在讨论Linux主机上到底需不需要开启swap空间,而且目前公有云主机默认都是把swap关了的,很多公司也是没有开启swap,那到底需不需要开启呢? 我之前在看<鸟哥的Linux ...