一.ThreadLoacl的理解:

官方的讲:

ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰

通俗的讲:

ThreadLocal也叫做线程本地变量,ThreadLoacl为变量在每个线程中的都创建了副本,每个线程可以访问自己内部的副本变量,线程之间互不影响

二.TreadLocal的原理:

从上图我们可以初步窥见ThreadLocal的核心机制:

1)每个Thread线程内部都有一个Map

2)Map里面储存线程本地对象key和线程的变量副本value

3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值

这样对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,这样就形成了副本隔离,互不干扰

三.ThreadLocal的底层源码

ThreadLocal类提供了以下几个核心方法:

1.get方法:获取当前线程的副本变量值

2.set方法:设置当前线程的副本变量值

3.remove方法:移除当前线程的副本变量值

4.initilaValue方法:初始化当前线程的副本变量值,初始化null

1)ThreadLocal.get():

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;
} private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
} protected T initialValue() {
return null;
}

源码解析:

1.获取当前线程的ThreadLocalMap对象threadLocals(实际储存副本值的Map)

2.Map不为空的话,从Map中获取线程储存的K-V Entry结点,然后从Entry结点中获取Value副本值返回

3.Map为空的话,返回初始值null,之后还需向Map中添加value为null的键值对,避免空指针异常

2.ThreadLocal.set():

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
} ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

源码解析:

1.获取当前线程的成员变量Map

2.Map不为空:重新将ThreadLocal对象和Value副本放入Map中

3.Map为空:对线程成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal对象和Value副本放入Map中

3.ThreadLocal.remove():

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
} ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

源码分析:

直接调用了ThreadLocalMap的remove方法(后面我们还会探究ThreadLocalMap类的底层源码!,这里先放着)

4.ThreadLocal.initialValue() :

protected T initialValue() {
return null;
}

就是直接返回null

小结一下:我们发现ThreadLocal的底层源码都有一个ThreadLocalMap类,那么ThreadLocalMap类的底层源码又是什么样子的呢?我们一起来看看吧!

四.ThreadLocalMap的底层源码分析

ThreadLocalMap是ThreadLocal内部的一个Map实现,然而它没有实现任何集合的接口规范,因为它仅供ThreadLocal内部使用,数据结构采用数组+开方地址法,Entry继承WeakRefrence,是基于ThreadLocal这种特殊场景实现的Map,它的实现方式很值得我们取研究!!

1.ThreadLocalMap中Entry的源码

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

源码分析:

1.Entry中key只能是ThreadLocal对象,被规定死了的

2.Entry继承了WeakRefrence(弱引用,生存周期只能活到下次GC前),但是只有Key是弱引用,Value并不是弱引用

ps:value既然不是弱引用,那么key在被回收之后(key=null)Value并没有被回收,如果当前线程被回收了那还好,这样value也和线程一起被回收了,要是当前线程是线程池这样的环境,线程结束没有销毁回收,那么Value永远不会被回收,当存在大量这样的value的时候,就会产生内存泄漏,那么Java 8中如何解决这个问题的呢?

解决办法:

以上是ThreadLocalMap的set方法,for循环遍历整个Entry数组,遇到key=null的就会替换,这样就不存在value内存泄漏的问题了!!!

2.ThreaLocalMap中key的HashCode计算

ThreaLocalMap的key是ThreaLocal,它不会传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode,源码如下:

private final int threadLocalHashCode = nextHashCode();

 private static AtomicInteger nextHashCode = new AtomicInteger();

 //1640531527 这是一个神奇的数字,能够让hash槽位分布相当均匀
private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

源码分析:

我们发现ThreaLocalMap的hashcode计算没有采用模长度的方法,没有采用拉链法,采用的是开放地址法,其槽位采用静态的AtomicInteger每次增加1640531527实现,冲突了则加1或者减1继续进行增加1640531527

我们把这个数叫做魔数,通过这个魔数我们可以位key产生完美的槽位分配,hahs冲突的次数很少

(据说魔数和黄金比例,斐波那契数列存在某种关系)

3.ThreaLocalMap中set方法:

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算槽位
// hash冲突时,使用开放地址法
// 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); if (k == key) { // key 相同,则覆盖value
e.value = value;
return;
} if (k == null) { // key = null,说明 key 已经被回收了,进入替换方法
replaceStaleEntry(key, value, i);
return;
}
}
// 新增 Entry
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
rehash(); // 扩容
}

源码分析:

1.先是计算槽位

2.Entry数组中存在需要插入的key,直接替换即可,存在key=null,也是替换(可以避免value内存泄漏)

3.Entry数组中不存在需要插入的key,也没有key=null,新增一个Entry,然后判断一下需不需要扩容和清除过期的值(关于扩容和清除过期值先不细讲)

4.ThreadLocalMap中getEntry方法:

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) // 无hash冲突情况
return e;
else
return getEntryAfterMiss(key, i, e); // 有hash冲突情况
}

源码分析:

1.计算槽位i,判断table[i]是否有目标key,没有(hahs冲突了)则进入getEntryAfterMiss方法

getEntryAfterMiss方法分析:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length; while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i); // 清除过期的slot
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

源码分析:

遇到hash冲突之后继续向后查找,并且会在查找路上清除过期的slot

5.ThreadLocalMap中rehash方法:

private void rehash() {
expungeStaleEntries(); // 清除过程中,size会减小,在此处重新计算是否需要扩容
// 并没有直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提前触发resize
if (size >= threshold - threshold / 4)
resize();
} private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}

源码分析:

先调用expungeStaleEntries()清除所有过期的slot,然后提前触发resize(约 threshold 的 3/4的时候)

下面看看resize():

private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0; for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
} setThreshold(newLen);
size = count;
table = newTab;
}

扩容2倍,同时在Entry移动过程中会清除一些过期的entry

6.ThreadLocal中的remove方法:

private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

源码分析:

遍历Entry数组寻找需要删除的ThreadLocal,建议在ThreadLocal使用完成之后再调用此方法

现在再详细分析一下ThreadLocalMap的set方法中的几个方法:

1.replaceStaleEntry方法:替换

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e; // 往前寻找过期的slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // 找到 key 或者 直到 遇到null 的slot 才终止循环
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // 如果找到了key,那么需要将它与过期的 slot 交换来维护哈希表的顺序。
// 然后可以将新过期的 slot 或其上面遇到的任何其他过期的 slot
// 给 expungeStaleEntry 以清除或 rehash 这个 run 中的所有其他entries。 if (k == key) {
e.value = value; tab[i] = tab[staleSlot];
tab[staleSlot] = e; // 如果存在,则开始清除前面过期的entry
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
} // 如果我们没有在向前扫描中找到过期的条目,
// 那么在扫描 key 时看到的第一个过期 entry 是仍然存在于 run 中的条目。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} // 如果没有找到 key,那么在 slot 中创建新entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); // 如果还有其他过期的entries存在 run 中,则清除他们
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上文中run的意思不好翻译,理解为开放地址中一个slot中前后不为null的连续entry

2.cleanSomeSlots方法:清除一些slot(按照规则清除“一些”slot,而不是全部)

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); // n = n / 2, 对数控制循环
return removed;
}

当新元素被添加时,或者另外一个过期元素已经被删除的时候,会调用该方法,该方法会试探性的扫描一些Entry寻找过期的条目,它执行对数数量的扫描,是一种基于不扫描(快速但保留垃圾)和所有元素扫描之间的平衡!!

对数数量的扫描!!!

这是一种折中的方案

3.expungeStaleEntry:清除

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // 清除当前过期的slot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // Rehash 直到 null 的 slot
Entry 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; while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

真正的清除,不仅会清除当前过期的slot,还会继续往后查询直到遇到null的slot为止,对于查询遍历中没有被回收的情况,做了一次rehash

推荐大佬的博客:https://www.cnblogs.com/micrari/p/6790229.html

写的太详细了,太强了,源码注释贼多

---------------------------------------------------------------------------------------------

没有下文了,这是底线…

深入源码理解ThreadLocal和ThreadLocalMap的更多相关文章

  1. 从源码理解 ThreadLocal

    为每个线程保存各自的拷贝,可以通过在Thread类中定义一个成员变量来保存每个线程值,这样也是线程安全的. 通过定义一个成员变量 sn 来实现,这里并没有使用ThreadLocal类来实现: publ ...

  2. Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步

    目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...

  3. 基于SpringBoot的Environment源码理解实现分散配置

    前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...

  4. jedis的源码理解-基础篇

    [jedis的源码理解-基础篇][http://my.oschina.net/u/944165/blog/127998] (关注实现关键功能的类)   基于jedis 2.2.0-SNAPSHOT   ...

  5. VUEJS2.0源码理解--优

    VUEJS2.0源码理解 http://jiongks.name/blog/vue-code-review/#pingback-112428

  6. AdvanceEast源码理解

    目录 文章思路 源码理解 一. 标签点形式 按顺序排列四个点,逆时针旋转,且第一个点为左上角点(刚开始选择最左边的点, 二. 标签切边 三. loss计算 四. NMS 最后说明 文章思路 大神的gi ...

  7. Pytorch学习之源码理解:pytorch/examples/mnists

    Pytorch学习之源码理解:pytorch/examples/mnists from __future__ import print_function import argparse import ...

  8. .NET Core 3.0之深入源码理解Startup的注册及运行

    原文:.NET Core 3.0之深入源码理解Startup的注册及运行   写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...

  9. 深入源码理解Spring整合MyBatis原理

    写在前面 聊一聊MyBatis的核心概念.Spring相关的核心内容,主要结合源码理解Spring是如何整合MyBatis的.(结合右侧目录了解吧) MyBatis相关核心概念粗略回顾 SqlSess ...

随机推荐

  1. 2018-08-11 中文代码示例之Spring Boot 2.0.3问好

    上次试用Spring Boot还是两年前: 中文代码示例之Spring Boot 1.3.3演示. 打算用在一个讨论组内小项目上, 于是从官网Building an Application with ...

  2. 中国最强AI超级服务器问世,每秒提供AI计算2000万亿次

    https://mp.weixin.qq.com/s/1EVczHp11OJ4GEjeE3z5cA 业内唯一以“AI计算”为核心的人工智能大会昨天发布了一份重要报告. 9月12日,<中国AI计算 ...

  3. 活字格对接RF手持机与ERP系统,帮助RFID快速盘点方案落地

    这个玩意有点意思,要不要搞来玩玩? ----------- 射频技术(RF)是Radio Frequency的缩写,较常见的应用有无线射频识别(Radio Frequency Identificati ...

  4. Android性能优化问题总结

    性能优化这块,分为UI性能优化.内存优化.数据库优化.网络优化.耗电优化等等.可以从1.如何发现问题,2.怎么解决问题,3.解决效果对比,这几个方面去描述.举个简单例子——UI优化,可以从 UI出现什 ...

  5. SQL中的ALL,ANY,SOME的用法

    准备两个表: --T1(2,3)--T2(1,2,3,4) --ALL,ANY,SOME 的子查询-- >ALL 父查询中的结果集大于子查询中每一个结果集中的值,则为真SELECT * FROM ...

  6. html常见标签和属性

    主体 body中常见属性 属性 表格 列表 表单 其他 input字段属性 form字段属性

  7. 洗礼灵魂,修炼python(64)--爬虫篇—re模块/正则表达式(2)

    前面学习了元家军以及其他的字符匹配方法,那得会用啊对吧?本篇博文就简单的解析怎么运用 正则表达式使用 前面说了正则表达式的知识点,本篇博文就是针对常用的正则表达式进行举例解析.相信你知道要用正则表达式 ...

  8. sql server连接oracle并实现增删改查

    需要一个软件ODAC112040Xcopy_64bit 我连接的oracle是11g r2  sqlserver 是 2016 软件下载 https://pan.baidu.com/s/1OpYmpR ...

  9. CentOS6.5 安装并配置vsftpd

    一.获取root权限 su 输入root密码 二.检查是否安装 rpm -qa | grep vsftpd 如果安装,会显示安装版本号,没有就什么都不显示 三.若已安装过vsftpd,先卸载.卸载前, ...

  10. 一个好看的php验证码源码

    <?php     $w = 80; //设置图片宽和高 $h = 26; $str = Array(); //用来存储随机码 $string = "ABCDEFGHIJKLMNOPQ ...