一、ThreadLock的内部结构

1.1 常见的误解

通常,如果我们不去看源代码的话,我猜ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。

1.2 核心结构

但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

  1. 每个Thread线程内部都有一个Map (ThreadLocalMap) ​
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

1.3 这样设计的好处

这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:

  1. 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
  2. 当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);}

代码流程:

  1. 获取当前线程,并获取当前线程的TreadLockMap实例(从getMap(Thread t)中很容易看出来)
  2. 如果获取到的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不为空时的执行逻辑

  1. 根据key的散列哈希计算Entry的数组下标
  2. 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
  3. 如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
  4. 如果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. 进程和线程基础知识(已经是最详细的啦)

    进程和线程基础知识 文章目录 进程和线程基础知识 一.前言 二.进程 1.引入 2.并发和并行有什么区别? 3.进程与程序的关系的类比 4.进程的状态 5.进程的控制结构 6.进程的控制 7.进程的上 ...

  2. Java 线程 - 基础及工具类 (二)

    Java 并发系列文章 Java 线程 - 并发理论基础(一) Java 线程 - 基础及工具类 (二) Java 线程 - 并发设计模式 (三) Java 线程(二) 通用的线程生命周期 Java ...

  3. C#多线程编程系列(二)- 线程基础

    目录 C#多线程编程系列(二)- 线程基础 1.1 简介 1.2 创建线程 1.3 暂停线程 1.4 线程等待 1.5 终止线程 1.6 检测线程状态 1.7 线程优先级 1.8 前台线程和后台线程 ...

  4. 二、基础入门——数据包扩展

    二.基础入门--数据包扩展(2022.11.7) 1. http和https区别 ​ http相当于明文,https相当于是加密过后的. HTTP HTTPS TCP SSL or TLS IP TC ...

  5. python删除线程,python线程基础

    一 基本概念 1 并行和并发 1 并行,parallel 同时做某些事,可以互不干扰的同一时刻做几件事 如高速公路上的车道,同一时刻,可以有多个互不干扰的车运行 在同一时刻,每条车道上可能同时有车辆在 ...

  6. Java中的线程基础知识

    Java中的线程基础知识 1.线程概念 线程是程序运行的基本执行单元.当操作系统(不包括单线程的操作系统,如微软早期的DOS)在执行一个程序时,会在系统中建立一个进程,而在这个进程中,必须至少建立一个 ...

  7. [CLR via C#]25. 线程基础

    原文:[CLR via C#]25. 线程基础 一.Windows为什么要支持线程 Microsoft设计OS内核时,他们决定在一个进程(process)中运行应用程序的每个实例.进程不过是应用程序的 ...

  8. 线程基础知识——Windows核心编程学习手札系列之六

    线程基础知识 --Windows核心编程学习手札系列之六 线程与进程一样由两部分构成:一是线程的内核对象,操作系统用它来对线程实施管理,也是系统用来存放线程统计信息的地方:二是线程堆栈,用于维护线程在 ...

  9. 线程基础知识系列(三)线程的同步

    本文是系列的第三篇,前面2篇,主要是针对单个线程如何管理,启动等,没有过多涉及多个线程是如何协同工作的. 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 ...

  10. linux线程基础篇----线程同步与互斥

    linux线程基础----线程同步与互斥 一.同步的概念 1.同步概念  所谓同步,即同时起步,协调一致.不同的对象,对"同步"的理解方式略有不同.如,设备同步,是指在两个设备   ...

最新文章

  1. linux下SVN不允许空白日志提交
  2. Fiddler抓包使用教程-断点调试
  3. 上古卷轴5python_基于Python-Flask的权限管理5:字典管理
  4. Java Web学习总结(10)学习总结-EL表达式
  5. automapper自动创建映射_ASP.NET Core教程:ASP.NET Core使用AutoMapper
  6. Mybatis判断表是否存在
  7. [汇编学习笔记][第十六章直接定址表]
  8. 局域网计算机如何传输文件,强烈推荐电脑同一个局域网传输文件的图文教程
  9. 使用国密函数读取金税盘信息
  10. 《国富论》阅读笔记03
  11. gaussdb 日常运维命令总结【01】
  12. “第二课堂”开课啦~
  13. [vue3.x]实战问题--Extraneous non-props attributes
  14. 适合学计算机用的机械键盘,什么是机械键盘 机械键盘和普通键盘的区别
  15. 痞子衡嵌入式:存储器大厂Micron的NOR Flash芯片特殊丝印设计(FBGA代码)
  16. 局域网、网段、子网的区别
  17. Direct3D中的绘制
  18. [Swift]LeetCode832. 翻转图像 | Flipping an Image
  19. 使用eNSP进入视图设置密码和IP
  20. 2021-10-02PE文件学习

热门文章

  1. detail.html翻译中文,detail是什么意思_detail的翻译_音标_读音_用法_例句_爱词霸在线词典...
  2. numpy的结构数组和内存布局
  3. 容器技术Docker K8s 50 容器镜像服务(ACR)详解-使用与实践
  4. iOS swift当app从后台切换到前台,或者锁屏后开启唤醒,app收到通知,didBecomeActiveNotification
  5. 爱情指数测试脸型软件,心理测试:你和谁的脸型最像?测出你的幸运指数是多少!...
  6. 检测1的个数_面部皮肤检测仪是美容院的新套路吗?
  7. 估计理论(7):应用BLUE的两个例子
  8. java打开文件对话框
  9. 总结一下用caffe跑图片数据的研究流程接上篇
  10. DNN与微软同声传译系统背后的故事