首页 > 其他分享 >ThreadLocal入门笔记

ThreadLocal入门笔记

时间:2024-05-12 09:30:55浏览次数:16  
标签:清理 入门 元素 笔记 ThreadLocal key Entry null

ThreadLocal入门笔记

最近学习小傅哥的面经手册,学习到ThreadLocal,这里做个笔记加深印象,也方便日后复习。

ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法,它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

一、ThreadLocal简单使用

public class ThreadLocalDemo {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str){
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " : " + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                localVar.set("localVar1");
                print("localVar1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                localVar.set("localVar2");
                print("localVar2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        thread1.start();
        thread2.start();

    }
}

打印结果:

localVar1 : localVar1
after remove : null
localVar2 : localVar2
after remove : null

一、ThreadLocal结构

Thread类中有ThreadLocalMap类的成员变量threadLocals,这变量由ThreadLoacl类维护。

ThreadLocalMapThreadLocal类的静态内部类,内部有一个Entry静态内部类,继承了WeakReference<ThreadLocal<?>>。所以存储在Entry中的ThreadLocal键是弱引用。

弱引用:当一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收。

ThreadLocalMap内部还定义了一个成员变量Entry[] table

所以ThreadLocal的整体结构:

【图片来源】:ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎

【图片来源】:ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎

二、如何存放元素

ThreadLocal 存放数据的底层数据结构:

图片来源:面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 | 小傅哥 bugstack 虫洞栈

ThreadLocal 使用的是斐波那契(Fibonacci)散列法 + 开发寻址存储数据到数组结构Entry[] table中。

ThreadLocale类 set(T value)

  1. 先获取当前线程的threadLocals(ThreadLocalMap类),如果threadLocals为空,就为该线程创建一个ThreadLocalMap对象,并将元素存放进去,赋给threadLoacls,否则就直接向threadLocals中添加元素。

    ThreadLocal set(T value)源码如下:

  1. map.set(this, value);

    mapthreadLocals中添加元素,其实操作的是map中的Entry数组tableThreadLocal是基于数组结构的开放寻址方式存储,那就一定会有哈希的计算,利用key值即当前ThreadLocal对象的threadLocalHashCode值计算下标,int i = key.threadLocalHashCode & (len-1);

    如果当前下标:

    1. 是空位置直接插入

    2. 不为空,key 相同,直接更新

    3. 不为空,key 不相同,开放寻址,e = tab[i = nextIndex(i, len)],也就是同一个下标位置发生冲突时,则 +1向后寻址,直到找到空位置或垃圾回收位置进行存储。

    4. 不为空,key == null,碰到过期key。遇到的是弱引用发生GC时产生的情况。碰到这种情况,ThreadLocal 会进行探测清理过期key,replaceStaleEntry(),探测式清理过期元素

    之后的以下这段代码会判断是否需要进行扩容。

    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
    

    ThreadLocalMap类 set(ThreadLocal<?> key, Object value)源码:

  2. 关于ThreadLocal类的threadLocalHashCode属性:

    ThreadLocal 使用的是斐波那契(Fibonacci)散列法计算哈希值。 0x61c88647,这是一个哈希值的黄金分割点,让散列分布更分散减少哈希碰撞。

流程图:(这图中拉链法,感觉该改为开放寻址法的线性探测?)

深入理解Hash表:拉链法与开放寻址法的比较-百度开发者中心

图片来源:面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 | 小傅哥 bugstack 虫洞栈

三、扩容机制

ThreadLocalMap类 set(ThreadLocal<?> key, Object value)源码的最后一段代码:

int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
  1. 进行启发式清理,cleanSonmeSlots()调用expungeStaleEntry() 清除过期元素。返回boolean值表示是否有清理过期元素,false:未清理,true:清理了过期元素。

    源码如下:

  2. 判断sz >= threshold,其中 threshold = len * 2 / 3,也就是说数组中填充的元素,大于 len * 2 / 3,并且如果执行完启发式清理工作后,未清理到任何数据,就需要扩容了。

  3. rehash(),扩容并重新计算元素位置。

    rehash()方法中先是调用了expungeStaleEntries(),探测式清理过期元素,以及判断清理后是否满足扩容条件,size >= threshold * 3/4。

    满足后执行扩容操作resize(),把旧数组中的元素重新散列分布到新的数组中。

    rehash()源码如下:

真正的进行扩容的操作resize()

  1. 创建一个容量为原来2倍大小的新Entry[]

  2. 遍历原来的Entry数组table,获取entry元素的对应的ThreadLocal,即key值。

  3. 检测key值的 if (k == null),将对应value=null,方便GC。

  4. 通过key值ThreadLocalthreadLoaclHashCode,重新计算下标,重新放到新数组中。

  5. 在放置数组的过程中,如果发生哈希碰撞,则调用nextIndex(),向后寻址。

resize()源码如下:

四、如何获取元素

ThreadLocal类 get()方法

  1. 先获取threadLoacls即map

  2. 如果map != null,获取当前ThreadLocal对象对应的Entry对象,map.getEntry(this)

    ThreadLocal get()源码如下:

  3. map.getEntry(this)

    1. 通过ThreadLocal对象的threadLocalHashCode,计算该ThreadLocal对象对应的Entry对象在Entry数组table中的下标,定位到对应的Entry对象;

    2. 直接定位到,没有哈希冲突,直接返回元素即可。if (e != null && e.get() == key);

    3. 没有直接定位到,return getEntryAfterMiss(key, i, e),获取Entry对象。

    ThreadLoaclMap类 getEntry(ThreadLocal<?>): Entry源码如下:

  4. getEntryAfterMiss(key, i, e)

    1. key不同,需要开放寻址,i = nextIndex(i, len);

    2. key不同,开放寻址,遇到GC清理元素,需要探测式清理,再寻找元素,if (k == null) expungeStaleEntry(i);

    ThreadLoaclMap类 getEntryAfterMiss(ThreadLocal<?>, int, Entry): Entry源码如下:

五、清理元素

探测式清理[expungeStaleEntry]

探测式清理,是以当前遇到的 GC 元素开始,向后不断的清理。直到遇到 null 为止,才停止 rehash 计算Rehash until we encounter null, for(...; (e = tab[i]) != null; ...)

for循环中:

  1. 获取当前Entry对象,对应的ThreadLocal对象,即Key值,ThreadLocal<?> k = e.get();

  2. 如果key值为null,已经被GC回收,清理,e.value = null; tab[i] = null;

  3. 如果key值不为null,计算下标。如果和当前在tab中的下标不同,将该Entry对象,放至新的下标位置。如果有哈希冲突,开放寻址。

    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;
    }
    

ThreadLoaclMap类 expungeStaleEntry(int): int源码如下:

启发式清理[cleanSomeSlots]

启发式清理,有这么一段注释,大概意思是;试探的扫描一些单元格,寻找过期元素,也就是被垃圾回收的元素。当添加新元素或删除另一个过时元素时,将调用此函数。它执行对数扫描次数,作为不扫描(快速但保留垃圾)和与元素数量成比例的扫描次数之间的平衡,这将找到所有垃圾,但会导致一些插入花费O(n)时间。

while 循环中不断的右移进行寻找需要被清理的过期元素,最终都会使用 expungeStaleEntry 进行处理,这里还包括元素的移位。

ThreadLoaclMap类 cleanSomeSlots(int, int): boolean源码如下:

问题:

  1. 为什么不直接重写hashcode(),而是定义threadLocalHashCode成员变量?

  2. 为什么 Entry 使用弱引用?

参考资料

ThreadLocal一个线程只能存放一个变量吗?想存多个怎么搞? - 苏三说技术的回答 - 知乎

面经手册 · 第12篇《面试官,ThreadLocal 你要这么问,我就挂了!》 | 小傅哥 bugstack 虫洞栈

Java中的ThreadLocal详解 - 夏末秋涼 - 博客园

标签:清理,入门,元素,笔记,ThreadLocal,key,Entry,null
From: https://www.cnblogs.com/zh-Note/p/18187496

相关文章

  • 读天才与算法:人脑与AI的数学思维笔记25_涌现理论
    1. 人工智能新闻1.1. 人工智能新闻报道算法的核心是如何将未经处理的原始数据转换成新闻报道1.2. 很少有记者为美联社决定使用机器来帮助报道这些新闻持反对意见1.2.1. 像“Wordsmith”这样的算法,具有自动化的洞察力、科学的叙事能力,现在正被应用于基于大量数据的分析报道......
  • 【vue3入门】-【22】 插槽
    插槽-基本使用方式我们已经了解了组件能够接收任意类型的JavaScript值作为props,但是组件要如何接收模版内容呢?在某些场景中,我们可能想要为子组件传递一些模版片段,让子组件在他们的组件中渲染这些片段。最基本的使用方式app.vue<template><!--单标签就是仅应用当前组件-->......
  • [NOI2009] 二叉查找树 & 笛卡尔树学习笔记
    这个题:二叉搜索树原理认识+区间dp;只要熟练相关算法就一定可以做出来。但我不行。。。我们学习一下笛卡尔树:什么垃圾东西,不学了。发现这个题是l蓝书上一道题jqb。二叉查找树又有一个性质:二叉查找树的中序遍历是其代表的序列从小到大排序的结果。而无论Treap如何旋转,其都......
  • 数据结构学习笔记-递归求解森林高度
    森林的高度递归求解问题描述:设计算法求解森林的高度【算法设计思想】两个函数,一个用于计算单个二叉树的高度,另一个用于计算二叉树森林(即一组二叉树)的最大高度。下面是对两个函数的详细解释:1.treeHeight函数这个函数用于计算单个二叉树的高度。二叉树的高度定义为从根节点到......
  • FFT 学习笔记
    应用FFT,中文“快速傅里叶变换”,用来加速多项式乘法和卷积,可以将\(O(n^2)\)的复杂度优化至\(O(n\logn)\)。多项式系数表示法一个\(n\)次\(n+1\)项的多项式\(f(x)\)可以表示为\(f(x)=\sum\limits_{i=0}^{n}a_ix^i\)。也可以用每一项的系数表示\(f(x)\),即\(f......
  • Windows 下 PyTorch 入门深度学习环境安装(CPU版本)
    Windows下PyTorch入门深度学习环境安装(CPU版本)一、安装Anaconda二、虚拟环境配置2.1基础命令列出虚拟环境condaenvlist创建虚拟环境https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/maincondacreate-n虚拟环境名字python=版本-c镜像地址激活环境conda......
  • PyTorch深度学习快速入门教程
    PyTorch深度学习快速入门教程一、基础知识1.1Python学习中的两大法宝1.2pycharm以及jupyter使用及对比将环境写入Notebook的kernel中:python-mipykernelinstall--user--name环境名称--display-name"Python(环境名称)"打开Jupyternotebook,新建Python文件,这时候......
  • Vue3笔记
    1.man.js文件//引入一个工厂函数createAppimport{createApp}from'vue'importAppfrom'./App.vue'//创建应用实例对象app---类似于vue2中的vm,但app比vm更'轻'constapp=createApp(App)console.log('app',app)app.mount('#app......
  • 笛卡尔树学习笔记
    笛卡尔树引入是一种二叉树,每个节点由一个二元组\((k,w)\)形成。\(k\)满足二叉搜索树的性质,\(w\)满足堆的性质。上面这棵笛卡尔树相当于把数组元素值当作键值\(w\),而把数组下标当作键值\(k\)。显然可以发现,这棵树的键值\(k\)满足二叉搜索树的性质,而键值\(w\)满足小根......
  • 复习笔记
    1.对变量延迟初始化延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null。当你对一个全局变量使用了lateinit关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,否则Kotlin将无法......