ThreadLocal底层源码解析

ThreadLocal:顾名思义的意思是本地线程或者局部线程的意思,其真正含义是希望多个线程之间拥有自己的局部变量,多个线程间拥有自己的私人变量,在多线程间不被共享,被线程单独享用,这就是ThreadLocal设计之初的原衷

因此,无论是操作系统级别还是编程语言中,我们都能看到ThreadLocal的设计实现.

1.ThreadLocal原理

ThreadLocal如何实现线程隔离?

具体来说,ThreadLocal在每个线程中维护了一个ThreadLocalMap对象。ThreadLocalMap是一个散列表,其中键是ThreadLocal变量的引用,值是ThreadLocal变量的值。

具体来看Get方法的实现:

1.1.Get方法

 public T get() {
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();
}
  1. 获取当前线程,并获取当前线程的散列映射,也就是存储value的地方,根据当前散列映射判断键值对是否存在,不存在则说明未初始化,之后调用getEntry方法获取ThreadLocalMap的entry,也就是存放键和值的地方,至于这个键值是什么,后面再看,如果没有拿到,就同时去进行初始化setInitialvalue.

1.2.setInitialValue初始化方法

在一开始线程的局部变量没初始化设置好的情况下,这个方法是一定会被调用的,因此了解他的内部实现是有必要的

 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);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
  1. 先初始化他内部的值,这个值默认情况下为null
  2. 然后获取当前线程,根据当前线程获取他的ThreadLocalMap
  3. 如果ThreadLocalMap存在则进行初始化赋值,如果不存在则创造
  4. 最后返回value.
  • :ThreadLocal并不是传统意义上的散列映射
  • set方法的实现类似

1.3.ThreadLocalMap的具体实现

static class ThreadLocalMap {

        /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap是一个静态内部类,同样内部也包含一个静态内部类Entry,实现其真正value的存储方式,并继承弱引用,因此Entry的真正的实现是一个简单的Object的对象去存储的value,除此之外还包括几个重要成员对象

        /**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0

用来修饰散列表的一些重要字段,散列表的真正实现是这个table数组,而每一个Entry中的value就是其Object对象,根据其内部构造实现,便可以明白这个K也就是键是其ThreadLocal对象本身,也就是这个引用

  • 接下来就是面对这个散列表时,value的具体存储实现了

1.4.ThreadLocalMap的set方法

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); for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get(); if (k == key) {
e.value = value;
return;
} if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
} tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
  1. 简言之就是获取当前ThreadLocal对象的散列映射,然后根据当前ThreadLocal计算哈希值确认索引位置
  2. 获取索引位置对应的Entry数组的对象位置,依次向下nextIndex循环,每次循环拿到对应Entry对象,如果有相同的哈希值的Entry对象,则将Entry对象的值赋值为value,如果找不到相同哈希值的Entry对象,那么调用replaceStaleEntry去替换旧的Entry对象
  3. 最后size++(前提是e!=null,也就是说Entry对象不存在,就不会在循环里面走,立刻跳出,赋值一个新的Entry对象),同时判断size字段是否大于threshold字段,并且cleanSomeSlots方法返回false,则进行rehash方法重新哈希数组

为什么需要调用cleanSomeSlots方法:这个方法的目的是为了清除一些旧的value对象,也就是Entry对象,底层他会去部分遍历这个散列表,直到n的值为0,也就是sz的值为0,也就是为了避免内存的占用,至于内存泄露后面再将


1.5.ThreadLocalMap的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对象,然后进行清除clear,这个方法实际上是将referent这个字段设置为null,是Reference中的一个字段,用来帮助我们进行GC回收的,expungeStaleEntry方法则是真正用来帮我们进行Entry对象和值的回收,设置为null.因此调用clear方法实际上就显式地回收了我们弱引用关联的对象,避免了内存泄漏的问题.而这个referent实际上也就是我们一开始对Entry对象进行初始化的ThreadLocal这个键.


1.6.ThreadLocalMap的getEntry方法

这个方法是真正去查找ThreadLocalMap中对饮Entry对象的方法,具体实现如下:

   private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
  • 根据哈希运算得到对应的索引位置,查找对应的Entry对象,如果找到符合条件就返回,如果没有就调用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);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
  • 这个方法其实也就是哈希表的开放地址法,没有类似HashMap去采用链地址开放去解决哈希冲突,因此会一次次向下去寻找,如果发现Entry对象的k为null,那么调用expungeStaleEntry方法,这个方法在此之前也出现过,简单的英文释义是:擦去稳定的Entry对象,具体实现后面在看.

1.7.ThreadLocalMap的expungeStaleEntry方法

这个方法的设计比较重要,主要是用于清除没有用的ThreadLocal,还有进行重新哈希的一个过程,具体实现如下:

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // 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) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
  1. 先将索引位置的key,value置空
  2. 第二步就是rehash去进行清除运算
  3. 所谓的清除就是对Entry对象的key为null的Entry对象进行一个回收,所谓的运算就是因为在set方法中,解决哈希冲突的实现是通过开放地址法去解决的,因此在某些Entry对象进行清理之后,这些对象的索引位置重新进行安排

1.8.ThreadLocalMap的cleanSomeSlots部分清除方法

在set方法中,这个方法有使用到,他的真正含义就是去部分清除一些对象

 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;
}
  1. 参数解释

    • int i:起始索引,指明从哪个位置开始检查。
    • int n:控制扫描范围的参数,方法会扫描大约n/2个插槽。
  2. 循环扫描
    • 方法使用一个do-while循环来遍历哈希表的一部分。
    • 在每次迭代中,它使用nextIndex(i, len)来移动到下一个索引。
    • 如果发现任何条目的ThreadLocal引用为null(意味着没有线程再使用它),则调用expungeStaleEntry(i)来清理这个条目,并重置扫描范围(n = len)。

可以看到这是一种均衡策略,在清除和时间效率上做出的一种决策,如果发现有引用为null的情况,就可能存在垃圾的问题,因此需要去重新调用expungeStaleEntry方法进行一个清除,因此这个方法的清理类似抽样调查

为什么在set方法最后,如果添加了一个新的Entry就需要去调用这个方法?

我的理解是这样的,如果没有定期去清除,就不能确保哈希表的健康和效率,只添加元素而不做任何监控,这对于任何一件事情来说都是一种不可控的风险.因此在时间上,对于我们整个Entry而言,也是局部抽样的方式去进行检查


1.9ThreadLocalMap的replaceStaleEntry方法

这个方法的实现相对于其他方法要复杂很多,其核心思想就是进行Entry的替换

具体实现如下:

 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e; // Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i; // Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value; tab[i] = tab[staleSlot];
tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
} // If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
} // If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

rehash之前的几步都很明白,不过多讲解,直接从Rehash看起

  • 我们要去思考在key为null的情况下,为什么会调用这个方法
  1. 可以看到这个for循环是向前遍历的,这是一个大前提,在第一个for循环中向前查找第一个key为null的情况,因为一旦遇到Entry对象存在的情况,就会退出,因此回过头去看set方法时是向后遍历,与这里是反向,也就是调用这个方法的前提是此Entry对象前的Entry对象的key存在且不是我要替换的key(也就不是相同的哈希值),但是由于此时此Entry对象前存在的对象如果发生GC的情况下,此时我们存放此Entry对象的位置应该放在前面那一个位置

  2. 因此第一遍扫描是解决了一个同一时间的问题,那下一次for循环又是解决什么问题,如果在我们对索引为2的位置(图中为null的第一个区域)此时进行了安置,然后前一项发生了GC进行了处理.现在来看循环退出条件是Entry对象不为null,也就是说如果遇到适合的位置,为null,则直接进行赋值,和我们的set方法有相似之处,如果没有说明此时的位置可能存在Entry对象了,后续操作就是如果找到了相同的Key,那么进行替换,同时if (slotToExpunge == staleSlot)如果满足,说明 staleSlot 之前没有需要清理的元素,那么就将 slotToExpunge 设置 i,意思是从当前元素开始进行清理,因为如果staleSlot之前的位置有需要清理的元素,两者就不会相等(参考第一个for循环).


2.Thread与ThreadLocal与ThreadLocalMap的关系

废话不多说,上图:

示例:

public class ThreadLocalExample {

    // 创建三个线程局部变量
private static final ThreadLocal<Integer> threadLocalVar1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocalVar2 = new ThreadLocal<>();
private static final ThreadLocal<Boolean> threadLocalVar3 = new ThreadLocal<>(); public static void main(String[] args) {
// 线程1
new Thread(() -> {
threadLocalVar1.set(100);
threadLocalVar2.set("Hello");
threadLocalVar3.set(true); System.out.println("Thread 1: " + threadLocalVar1.get());
System.out.println("Thread 1: " + threadLocalVar2.get());
System.out.println("Thread 1: " + threadLocalVar3.get());
}).start(); // 线程2
new Thread(() -> {
threadLocalVar1.set(200);
threadLocalVar2.set("World");
threadLocalVar3.set(false); System.out.println("Thread 2: " + threadLocalVar1.get());
System.out.println("Thread 2: " + threadLocalVar2.get());
System.out.println("Thread 2: " + threadLocalVar3.get());
}).start();
}
}

因此我们实际上发现其实ThreadLocal可以在不同的线程之间进行复用,只不过这个具体存储的value只和每个线程独有的Entry有关.


3.ThreadLocal的内存泄露问题

这个问题看到这里其实就可以很容易的理解了,因为对于Entry对象而言,他的key作为ThreadLocal引用,是一个弱引用对象,也就是说当ThreadLocal对象没有在被强引用对象引用的时候,当触发GC就会进行垃圾回收,但Entry对象中的value对象也就是Object对象是未被回收的一个状态,就可能导致内存垃圾的存在,导致内存泄漏问题.

如何解决内存泄漏问题

在次之前我们看到了如果手动调用remove方法是可以避免内存泄漏的问题,因此最简单的方法就是手动调用remove方法进行垃圾回收.

ThreadLocal底层源码解析的更多相关文章

  1. Java泛型底层源码解析-ArrayList,LinkedList,HashSet和HashMap

    声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...

  2. Java泛型底层源码解析--ConcurrentHashMap(JDK1.7)

    1. Concurrent相关历史 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全 ...

  3. ThreadLocal<T> 源码解析

    在activeJDBC框架内部的实现中看到了 ThreadLocal 这个类,记录下了每个线程独有的连接 private static final ThreadLocal<HashMap< ...

  4. ajax 底层源码解析

    对象: XMLHttpRequest 属性:readyState请求状态,开始请求时值为0直到请求完成这个值增长到4 responseText目前为止接收到的响应体,readyState<3此属 ...

  5. Disruptor底层源码解析(九)

    架构图: 性能为什么这么牛逼: public void sendData(ByteBuffer data) { //1 在生产者发送消息的时候, 首先 需要从我们的ringBuffer里面 获取一个可 ...

  6. Java 8 ThreadLocal 源码解析

    Java 中的 ThreadLocal是线程内的局部变量, 它为每个线程保存变量的一个副本.ThreadLocal 对象可以在多个线程中共享, 但每个线程只能读写其中自己的副本. 目录: 代码示例 源 ...

  7. Java ThreadLocal 的使用与源码解析

    GitHub Page: http://blog.cloudli.top/posts/Java-ThreadLocal-的使用与源码解析/ ThreadLocal 主要解决的是每个线程绑定自己的值,可 ...

  8. ThreadLocal源码解析-Java8

    目录 一.ThreadLocal介绍 1.1 ThreadLocal的功能 1.2 ThreadLocal使用示例 二.源码分析-ThreadLocal 2.1 ThreadLocal的类层级关系 2 ...

  9. EventBus源码解析 源码阅读记录

    EventBus源码阅读记录 repo地址: greenrobot/EventBus EventBus的构造 双重加锁的单例. static volatile EventBus defaultInst ...

  10. Android -- 从源码解析Handle+Looper+MessageQueue机制

    1,今天和大家一起从底层看看Handle的工作机制是什么样的,那么在引入之前我们先来了解Handle是用来干什么的 handler通俗一点讲就是用来在各个线程之间发送数据的处理对象.在任何线程中,只要 ...

随机推荐

  1. cpu分布式训练论文阅读

    Large Scale Distributed Deep Networks Downpour SGD: 模型的副本采用异步方式从参数服务器(Parameter Server)中获取参数w和上传Δw到参 ...

  2. IOS 16 无法打开开发版或者企业版本APP解决方案 - 需要开启开发者模式

    在IOS 16系统上,打开开发版本APP,或者企业版本APP时,会看到如下的提示信息: 需要开启开发者模式, xxx 需要在开发者模式下运行. 启用开发者模式前, 此App不可用 这个时由于IOS 1 ...

  3. 使用HTML一键打包APK工具打包KRPANO全景项目

    "HMTL一键打包APK工具"可以把本地HTML项目或者网站打包为一个安卓应用APK文件,无需编写任何代码,支持在安卓设备上安装运行. 打包工具群:429338543 下载地址: ...

  4. 解密Prompt系列15. LLM Agent之数据库应用设计:DIN & C3 & SQL-Palm & BIRD

    上一章我们主要讲搜索引擎和LLM的应用设计,这一章我们来唠唠大模型和DB数据库之间的交互方案.有很多数据平台已经接入,可以先去玩玩再来看下面的实现方案,推荐 sql translate:简单,文本到S ...

  5. PostgreSQL学习笔记-7.基础知识:子查询、自增、PRIVILEGES 权限

    子查询 子查询或称为内部查询.嵌套查询,指的是在 PostgreSQL 查询中的 WHERE 子句中嵌入查询语句.一个 SELECT 语句的查询结果能够作为另一个语句的输入值.子查询可以与 SELEC ...

  6. IEEE 国际计算科学与工程会议 (CSE-2023)

    随着计算机系统变得越来越庞大和复杂,基于数据的计算技术在支持下一代科学和工程应用方面发挥着关键作用.如今,科学和工程中基于云的复杂大数据应用由异构软件/硬件/网络组件组成,这些组件的容量.可用性和环境 ...

  7. 为.NET打开新大门:OpenVINO.NET开源项目全新发布

    为.NET打开新大门:OpenVINO.NET开源项目全新发布 在AI的应用越来越广泛的今天,优化深度学习模型并进行推理部署已经成为了一门必要的技术.Intel开发的OpenVINO工具包(Open ...

  8. 虹科案例 | 石油天然气行业CFD高性能计算解决方案

    公司简介 DNV GL 是全球领先的能源.石油和海事行业风险管理及资产绩效提升的软件供应商,主要为客户提供全面的风险管理和各类评估认证服务,认证涉及信息通信技术.汽车及航空天.食品与饮料.医疗等方面. ...

  9. svn 分支的创建及合并

    http://blog.csdn.net/jixiuffff/article/details/5586858 http://zhidao.baidu.com/link?url=uiRk-4ZBkLPx ...

  10. animate.css 动画库的下载与使用

    作者:WangMin 格言:努力做好自己喜欢的每一件事 animate.css是什么? animate.css 是一个有趣,酷炫的,跨浏览器的动画库,里面包含了许多常用的css动画.你可以将它用于你的 ...