线程基础阶段性总结和扩展(二)——ThreadLock
一、ThreadLock的内部结构
1.1 常见的误解
通常,如果我们不去看源代码的话,我猜ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。
1.2 核心结构
但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
哈希表,这个哈希表的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
- 每个Thread线程内部都有一个Map (ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
1.3 这样设计的好处
这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:
- 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
二、TreadLock源码分析
除了构造之外, ThreadLocal对外暴露的方法有以下4个:
方法声明 | 描述 |
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
2.1 结构
2.1.1 ThreadLocalMap成员变量
//初始容量——必须是2的幂
private static final int INITIAL_CAPACITY = 16;
//存放数据的table,,Entry类的定义在下面分析
// 同样,数组长度必须是2的冥。
private Entry[] table;//数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子
private int size = 0;//进行扩容的阈值,表使用量大于它的时候进行扩容。
private int threshold; // Default to 0//定义为长度的2/3
private void setThreshold(int len) {threshold = len * 2 / 3;
}
2.1.2 ThreadLocalMap存储结构——Entry
/*** Entry继承WeakReference,并且用ThreadLocal作为key.如果key为null* (entry.get() == null)表示key不再被引用,表示ThreadLocal对象被回收* 因此这时候entry也可以从table从清除。*/
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的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。
关于WeakReference可以参考这篇博客,关于Java中的WeakReferencea。
2.2 ThreadLocal的set方法以及set相关方法
/**设置当前线程对应的ThreadLocal值
*/
public void set(T value) {Thread t = Thread.currentThread();、// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)// 存在则调用map.set设置此实体entrymap.set(this, value);else// 1)当前线程Thread 不存在ThreadLocalMap对象// 2)则调用createMap进行ThreadLocalMap对象的初始化// 3)并将此实体entry作为第一个值存放至ThreadLocalMap中createMap(t, value);}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}
void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
代码流程:
- 获取当前线程,并获取当前线程的TreadLockMap实例(从getMap(Thread t)中很容易看出来)
- 如果获取到的map实例不为空,调用map.set()方法,否则调用createMap(t,value),createMap(t,value)内部是调用构造函数 ThreadLocal.ThreadLocalMap(this, firstValue)实例化map
可以看出来线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。下面来看看构造函数ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) 。
2.2.1 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) 和斐波那契数列
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new Entry[INITIAL_CAPACITY];//计算索引int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//设置值table[i] = new Entry(firstKey, firstValue);size = 1;//设置阈值setThreshold(INITIAL_CAPACITY);}
主要说一下计算索引,firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1):
- 关于“&(INITIAL_CAPACITY - 1)”,这是一种取模的方式,对于2的幂作为模数取模,用此代替
%(2^n)
,这也就是为啥容量必须为2的冥,在这个地方也得到了解答,至于为什么可以这样这里不过多解释,原理很简单。 - 关于firstKey.threadLocalHashCode,这是获取经过处理后的hashCode,与HashMap中的HashCode是不同的,下面会讲到。
2.2.2 firstKey.threadLocalHashCode和斐波那契数列
private final int threadLocalHashCode = nextHashCode();private static AtomicInteger nextHashCode =new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647
,关于这个值和斐波那契散列
有关,其原理这里不再深究,感兴趣可自行搜索,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table
中。
斐波那契数列demo:证明斐波那契数列是可以使哈希码均匀分布
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {magicHash(16);magicHash(32);
}
private static void magicHash(int size){int hashCode=0;for(int i=0;i<size;i++){hashCode=i*HASH_INCREMENT+HASH_INCREMENT;System.out.print((hashCode&(size-1))+" ");} System.out.println("");
}
运行结果:
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
2.2.3 ThreadLocalMap中的set()
前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下map不为空时的执行逻辑
- 根据key的散列哈希计算Entry的数组下标
- 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
- 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
- 如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据rehash扩容
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//根据哈希码和数组长度求元素放置的位置,即数组下标;上面说过int i = key.threadLocalHashCode & (len-1);/*从i开始往后一直遍历到数组最后一个Entry(线性索引)*在没有return的情况下,就使用nextIndex()获取下一个(线性探测法)*/for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//如果key相等,覆盖valueif (k == key) {e.value = value;return;}//如果key为null,用新key、value覆盖,同时清理历史key=null的无效entry(弱引用)if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;//如果超过阈值,扩容/*** cleanSomeSlots用于清除那些e.get()==null,也就是table[index] != null && table[index].get()==null* 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()*/if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
线性探测,是用来解决hash冲突的一种策略。它是一种开放寻址策略,
我想大家应该都知道hash表,它是根据key进行直接访问的数据结构,也就是说我们可以通过hash函数把key映射到hash表中的一个位置来访问记录,从而加快查找的速度。存放记录的数据就是hash表(散列表)当我们针对一个key通过hash函数计算产生的一个位置,在hash表中已经被另外一个键值对占用时,那么线性探测就可以解决这个冲突,这里分两种情况。
- 写入: 查找hash表中离冲突单元最近的空闲单元,把新的键值插入到这个空闲单元
- 查找: 根据hash函数计算的一个位置处开始往后查找,指导找到与key对应的value或者找到空的单元。
画图描述过程:
从上面的代码,我们可以看的,如果key=null,会调用replaceStaleEntry方法清除陈年数据,下面我们来看看这一块。
2.2.4 ThreadLockMap的replaceStaleEntry方法
/*替换脏的Entry*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;/*** 根据传入的无效entry的位置(staleSlot),向前扫描* 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),* 直到找到一个无效entry,或者扫描完也没找到。*/int slotToExpunge = staleSlot;//之后用于清理的起点for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))//e.get()表示key是不是等于null!!!下面有解释if (e.get() == null)slotToExpunge = i;/*** 向后扫描一段连续的entry*/for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();/*** 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换*/if (k == key) {e.value = value;//脏的位置和新的数据进行交换tab[i] = tab[staleSlot];tab[staleSlot] = e;//如果向前查找没有找到无效entry,则更新slotToExpunge为当前值iif (slotToExpunge == staleSlot)slotToExpunge = i;cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}/*** 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i*/if (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}/*** 如果没有找到key,也就是说key之前不存在table中* 就直接最开始的无效entry——tab[staleSlot]上直接新增即可*/tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);/*** slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。*/if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}
" if (e.get() == null)
slotToExpunge = i;"这一行代码表示key是不是为null,从哪里看出来的呢,请继续往下看
e是ThreadLocalMap的Entry类型,它的继承关系如下:
e.get()实际上是调用父类Reference.get()方法:
根据此方法注释翻译:如果这个引用对象有,返回这个引用对象的引用对象。
已被程序或垃圾收集器清除,该方法返回<code>null</code>。
2.2.5 ThreadLockMap的replaceStaleEntry方法图解
请根据上述2.2.4代码理解向前查找过程:
请根据上述2.2.4代码理解向后查找过程:
这两张图,再包括set()方法的图,就是ThreadLockMap线性探测的过程。
TreadLock有可能会导致内存泄漏,以及解决方法:
在哪里设置value=null,tab[i]=null的呢?
——replaceStaleEntry方法,会调用expungeStaleEntry方法,这里面会设置,代码位置如下图:
当然解决内存泄漏,最好是我们程序员在合适的地方调用ThreadLock.remove()方法。
ThreadLock.remove()里调用了expungeStaleEntry方法。
2.2.6 expungeStaleEntry方法、cleanSomeSlots方法、resize方法、expungeStaleEntries方法源码
expungeStaleEntry方法源码:
/*** 连续段清除* 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],* 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),* 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.** @param staleSlot key为null,需要无效entry所在的table中的索引* @return 返回下一个为空的solt的索引。*/private int expungeStaleEntry(int staleSlot) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 清理无效entry,置空tab[staleSlot].value = null;tab[staleSlot] = null;//size减1,置空后table的被使用量减1size--;ThreadLocal.ThreadLocalMap.Entry e;int i;/*** 从staleSlot开始向后扫描一段连续的entry*/for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();//如果遇到key为null,表示无效entry,进行清理.if (k == null) {e.value = null;tab[i] = null;size--;} else {//如果key不为null,计算索引int h = k.threadLocalHashCode & (len - 1);/*** 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]* 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。*/if (h != i) {tab[i] = null;while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}//下一个为空的solt的索引。return i;}
cleanSomeSlots方法源码::
/*** 启发式的扫描清除,扫描次数由传入的参数n决定** @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)** @param n 控制扫描次数,正常情况下为 log2(n) ,* 如果找到了无效entry,会将n重置为table的长度len,进行段清除。** map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len** @return true if any stale entries have been removed.*/private boolean cleanSomeSlots(int i, int n) {boolean removed = false;ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);ThreadLocal.ThreadLocalMap.Entry e = tab[i];if (e != null && e.get() == null) {//重置n为lenn = len;removed = true;//依然调用expungeStaleEntry来进行无效entry的清除i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);//无符号的右移动,可以用于控制扫描次数在log2(n)return removed;}
rehash方法源码:
private void rehash() {//全清理expungeStaleEntries();/*** threshold = 2/3 * len* 所以threshold - threshold / 4 = 1en/2* 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。* 判断的时候把阈值调低了。*/if (size >= threshold - threshold / 4)resize();}
expungeStaleEntries方法源码:
/*** 全清理,清理所有无效entry*/private void expungeStaleEntries() {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {ThreadLocal.ThreadLocalMap.Entry e = tab[j];if (e != null && e.get() == null)//使用连续段清理expungeStaleEntry(j);}}
2.3 get方法以及get相关方法
同样的对于ThreadLocalMap中的getEntry()也从ThreadLocal的get()方法入手。
public T get() {//同set方法类似获取对应线程中的ThreadLocalMap实例Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//为空返回初始化值return setInitialValue();
}
/*** 初始化设值的方法,可以被子类覆盖。*/
protected T initialValue() {return null;
}private T setInitialValue() {//获取初始化值,默认为null(如果没有子类进行覆盖)T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);//不为空不用再初始化,直接调用set操作设值if (map != null)map.set(this, value);else//第一次初始化,createMap在上面介绍set()的时候有介绍过。createMap(t, value);return value;
}
2.3.1ThreadLocalMap中的getEntry()
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {//根据key计算索引,获取entryint i = key.threadLocalHashCode & (table.length - 1);ThreadLocal.ThreadLocalMap.Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}/*** 通过直接计算出来的key找不到对于的value的时候适用这个方法.*/private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal<?> k = e.get();if (k == key)return e;if (k == null)//清除无效的entryexpungeStaleEntry(i);else//基于线性探测法向后扫描i = nextIndex(i, len);e = tab[i];}return null;}
2.3.2 ThreadLocalMap中的remove()
private void remove(ThreadLocal<?> key) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;//计算索引int i = key.threadLocalHashCode & (len-1);//进行线性探测,查找正确的keyfor (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//调用weakrefrence的clear()清除引用e.clear();//连续段清除expungeStaleEntry(i);return;}}}
三、总结
本文重点:
- TreadLocal和TreadLocalMap的结构
- set方法运行流程和流程图
- 线性探测过程
线程基础阶段性总结和扩展(二)——ThreadLock相关推荐
- 进程和线程基础知识(已经是最详细的啦)
进程和线程基础知识 文章目录 进程和线程基础知识 一.前言 二.进程 1.引入 2.并发和并行有什么区别? 3.进程与程序的关系的类比 4.进程的状态 5.进程的控制结构 6.进程的控制 7.进程的上 ...
- Java 线程 - 基础及工具类 (二)
Java 并发系列文章 Java 线程 - 并发理论基础(一) Java 线程 - 基础及工具类 (二) Java 线程 - 并发设计模式 (三) Java 线程(二) 通用的线程生命周期 Java ...
- C#多线程编程系列(二)- 线程基础
目录 C#多线程编程系列(二)- 线程基础 1.1 简介 1.2 创建线程 1.3 暂停线程 1.4 线程等待 1.5 终止线程 1.6 检测线程状态 1.7 线程优先级 1.8 前台线程和后台线程 ...
- 二、基础入门——数据包扩展
二.基础入门--数据包扩展(2022.11.7) 1. http和https区别 http相当于明文,https相当于是加密过后的. HTTP HTTPS TCP SSL or TLS IP TC ...
- python删除线程,python线程基础
一 基本概念 1 并行和并发 1 并行,parallel 同时做某些事,可以互不干扰的同一时刻做几件事 如高速公路上的车道,同一时刻,可以有多个互不干扰的车运行 在同一时刻,每条车道上可能同时有车辆在 ...
- Java中的线程基础知识
Java中的线程基础知识 1.线程概念 线程是程序运行的基本执行单元.当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个 ...
- [CLR via C#]25. 线程基础
原文:[CLR via C#]25. 线程基础 一.Windows为什么要支持线程 Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例.进程不过是应用程序的 ...
- 线程基础知识——Windows核心编程学习手札系列之六
线程基础知识 --Windows核心编程学习手札系列之六 线程与进程一样由两部分构成:一是线程的内核对象,操作系统用它来对线程实施管理,也是系统用来存放线程统计信息的地方:二是线程堆栈,用于维护线程在 ...
- 线程基础知识系列(三)线程的同步
本文是系列的第三篇,前面2篇,主要是针对单个线程如何管理,启动等,没有过多涉及多个线程是如何协同工作的. 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 ...
- linux线程基础篇----线程同步与互斥
linux线程基础----线程同步与互斥 一.同步的概念 1.同步概念 所谓同步,即同时起步,协调一致.不同的对象,对"同步"的理解方式略有不同.如,设备同步,是指在两个设备 ...
最新文章
- linux下SVN不允许空白日志提交
- Fiddler抓包使用教程-断点调试
- 上古卷轴5python_基于Python-Flask的权限管理5:字典管理
- Java Web学习总结(10)学习总结-EL表达式
- automapper自动创建映射_ASP.NET Core教程:ASP.NET Core使用AutoMapper
- Mybatis判断表是否存在
- [汇编学习笔记][第十六章直接定址表]
- 局域网计算机如何传输文件,强烈推荐电脑同一个局域网传输文件的图文教程
- 使用国密函数读取金税盘信息
- 《国富论》阅读笔记03
- gaussdb 日常运维命令总结【01】
- “第二课堂”开课啦~
- [vue3.x]实战问题--Extraneous non-props attributes
- 适合学计算机用的机械键盘,什么是机械键盘 机械键盘和普通键盘的区别
- 痞子衡嵌入式:存储器大厂Micron的NOR Flash芯片特殊丝印设计(FBGA代码)
- 局域网、网段、子网的区别
- Direct3D中的绘制
- [Swift]LeetCode832. 翻转图像 | Flipping an Image
- 使用eNSP进入视图设置密码和IP
- 2021-10-02PE文件学习
热门文章
- detail.html翻译中文,detail是什么意思_detail的翻译_音标_读音_用法_例句_爱词霸在线词典...
- numpy的结构数组和内存布局
- 容器技术Docker K8s 50 容器镜像服务(ACR)详解-使用与实践
- iOS swift当app从后台切换到前台,或者锁屏后开启唤醒,app收到通知,didBecomeActiveNotification
- 爱情指数测试脸型软件,心理测试:你和谁的脸型最像?测出你的幸运指数是多少!...
- 检测1的个数_面部皮肤检测仪是美容院的新套路吗?
- 估计理论(7):应用BLUE的两个例子
- java打开文件对话框
- 总结一下用caffe跑图片数据的研究流程接上篇
- DNN与微软同声传译系统背后的故事