并发编程 - ThreadLocal

news2024/12/27 11:55:40

前言

ThreadLocal 用于解决多线程对于共享变量的访问带来的安全性问题。ThreadLocal 存储线程局部变量。每个线程内置 ThreadLocalMap,ThreadLocalMap 的 key 存储 ThreadLocal 实例,value 存储自定义的值。与同步机制相比,它是一种“空间换时间”的方式,可以做到“访问并行化,对象专享化”。缺点是如果使用不当,则容易发生内存泄漏。

内存泄漏:指程序中已动态分配的堆内存由于某种原因未释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

ThreadLocal 的生命周期与线程相同。如果线程一直存活,并且 ThreadLocal 实例可以被访问,那么线程会一直持有 ThreadLocal 实例的副本的弱引用。一旦线程的生命周期结束,所有与 ThreadLocal 实例相关的副本都会被回收,除非有其它地方一直引用这些副本。

为什么是弱引用

使用强引用、弱引用两种引用方式作为对比。

强引用:一直存活,除非 GC Roots 不可达。只要强引用指向一个对象,则该对象表示存活,也就不会被垃圾回收。

弱引用:只能存活到下一次垃圾回收前。垃圾收集器一旦发现某个对象只具有弱引用,就会将该对象回收。

在这里插入图片描述

假设 Entry 的 key 使用强引用,如果没有手动删除这个 Entry,并且线程还在运行。在业务代码中使用完 ThreadLocal,然后 ThreadLocalRef 被回收。此时依然有强引用链 CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry,因此 Entry 不会被回收。并且 Entry 的 key 一直持有对 ThreadLocal 实例的强引用,导致 ThreadLocal 实例不会被回收,从而导致 ThreadLocal 的内存泄漏。

假设 Entry 的 key 使用弱引用,如果没有手动删除这个 Entry,并且线程还在运行。在业务代码中使用完 ThreadLocal,然后 ThreadLocalRef 被回收。由于 Entry 的 key 只持有 ThreadLocal 实例的弱引用,没有任何强引用指向 ThreadLocal 实例,所以 ThreadLocal 实例可以顺利被回收。此时 Entry 的 key 为 null,但是依然有强引用链 CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> value,因此 value 不会被回收。此外无法通过为 null 的 key 找到对应的 value,也就是说 value 无法被访问到,最终导致 value 内存泄漏。

上述两种情况都会有内存泄漏的问题。但是它们都有两个前提:没有手动删除这个 Entry、当前线程一直在运行。

因此,解决内存泄漏也可以针对这两个前提。

  • 只要使用完 ThreadLocal,就调用 remove 方法删除对应的 Entry。
  • 使用完 ThreadLocal,就让对应的线程随之结束。

相较于强引用,弱引用发生内存泄漏的实质上是无法根据为 null 的 key 找到对应的 value。而在 ThreadLocalMap 中的 set、getEntry、remove 方法会对 key 是否为 null 进行判断,如果为 null, 就会对对应的 value 设置为 null。这样就算忘记调用 ThreadLocal 的 remove 方法,弱引用也会比强引用多了一层保障。ThreadLocal 的 set、get、remove 方法底层也会分别调用 ThreadLocalMap 的 set、getEntry。也就是说,ThreadLocalMap 的 Entry 的 key 为 null 时,其 value 可以在下一次调用 ThreadLocal 的 get、set、remove 任一方法时都会被清除,从而避免了内存泄漏问题。

解决哈希冲突

ThreadLocalMap 使用闭散列(或者开放地址法、线性探测法)解决哈希冲突。具体的每次探测下一个地址,直到有空的地址可以插入,如果整个空间都没有空余的地址可以插入,就会产生内存溢出。

假设当前 table 长度为 16,也就是说如果计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且 key 与当前 key 不一致,那么就发生了哈希冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,这个时候如果还是冲突会回到 0,取 table[0],以此类推直到可以插入。

内存溢出:系统中没有足够的内存提供给申请者使用。

使用

在《Java 开发手册-嵩山版》中,关于 ThreadLocal 的注意事项如下:

必须回收自定义的 ThreadLocal 变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量使用 try-finally 块中进行回收。

e.g.

public class ThreadLocalDemo {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

    public static void main(String[] args) {
      	// 输出1
        System.out.println(threadLocal.get());
        threadLocal.set(threadLocal.get() + 1);
      	// 输出2
        System.out.println(threadLocal.get());
        threadLocal.remove();
      	// 输出1(删除之后,得到的是初始值)
        System.out.println(threadLocal.get());
    }
}

public class ThreadLocalDemo2 implements Runnable {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    private Integer getNextNum() {
        threadLocal.set(threadLocal.get() + 1);
        return threadLocal.get();
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + getNextNum());
        }
    }

    public static void main(String[] args) {
        ThreadLocalDemo instance = new ThreadLocalDemo();
        for (int i = 0; i < 3; i++) {
          	// 每个线程依次输出1、2、3
            new Thread(instance).start();
        }
      	// 输出0
        System.out.println(threadLocal.get());
    }
}

public class ThreadLocalDemo3 {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                threadLocal.remove();
            }
        };
    }
}

源码分析

先看下 ThreadLocal 的 set 方法,如下:

public void set(T value) {
    Thread t = Thread.currentThread();
  	// 获取线程的ThreadLocalMap属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

第一次调用会走 createMap 方法。

void createMap(Thread t, T firstValue) {
  	// 实例化 ThreadLocalMap,其中 key 为 ThreadLocal 实例,value 为自定义的值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

接下来看下 ThreadLocalMap 的 set 方法,如下:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 的下标位置
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         // i:(i + 1 < len) ? i + 1 : 0
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
		// 如果 key 为 null,则调用 replaceStaleEntry 方法将 value 设置为 null
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 将 key、value 插入到 Entry 中
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

然后看下 replaceStaleEntry 方法的处理,如下:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;
  
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }
	// 将 value 设置为 null
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

接下来看下 ThreadLocal 的 get 方法,如下:

public T get() {
    Thread t = Thread.currentThread();
  	// 获取线程的ThreadLocalMap属性
    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();
}

然后看下 setInitialValue 方法的处理,如下:

private T setInitialValue() {
  	// 获取初始值,默认 null
    T value = initialValue();
    Thread t = Thread.currentThread();
  	// 获取线程的ThreadLocalMap属性
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
      	// 实例化 ThreadLocalMap,其中 key 为 ThreadLocal 实例,value 为自定义的值
        createMap(t, value);
    return value;
}

接下来看下 ThreadLocalMap 的 getEntry 方法,如下:

private Entry getEntry(ThreadLocal<?> key) {
	// 计算 key 对应的下标位置
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果命中,则返回对应的 Entry
    if (e != null && e.get() == key)
        return e;
    // 如果没有命中,则调用 getEntryAfterMiss 方法
    else
        return getEntryAfterMiss(key, i, e);
}

然后看下 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;
      	// 如果 key 为 null,则调用 expungeStaleEntry 方法,将 value 设置为 null
        if (k == null)
            expungeStaleEntry(i);
        else
          	// i:(i + 1 < len) ? i + 1 : 0
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

看下 expungeStaleEntry 方法:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
  	// 将指定下标位置的 Entry 的 value 设置为 null
    tab[staleSlot].value = null;
  	// 将指定下标位置的 Entry 设置为 null
    tab[staleSlot] = null;
    size--;
    Entry e;
    int i;
  	// 从指定下标位置开始遍历
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
      	// 如果 key 为 null,则将 value 设置为 null
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
          	// 重新计算 key 的下标位置
            int h = k.threadLocalHashCode & (len - 1);
          	// 如果下标位置发生变化
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 将 Entry 迁移到新的位置
                tab[h] = e;
            }
        }
    }
    return i;
}

最后看下 ThreadLocal 的 remove 方法,如下:

public void remove() {
  	// 获取线程的ThreadLocalMap属性
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

接着看下 ThreadLocalMap 的 remove 方法,如下:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
  	// 计算 ThreadLocal 实例在 ThreadLocalMap 的下标位置
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
          	// 清除 Entry
            e.clear();
          	// 调用 expungeStaleEntry 方法
          	// 如果 key 为 null,则将 value 设置为 null
            expungeStaleEntry(i);
            return;
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/150322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue性能优化之预渲染prerender-spa-plugin+vue-meta-info解决seo问题

单页面应用中&#xff0c;web项目只有一个页面&#xff0c;前端根据路由不同进行组件之间的对应切换&#xff0c;动态的渲染页面内容。这就是客户端渲染&#xff0c;具有减少服务器端压力、响应速度快等优点。但是单页应用在优化用户体验的同时&#xff0c;也给我们带来了一些对…

阅读 | 001《人工智能导论》(三)知识应用篇1

文章目录知识应用第9章、专家系统9.1 专家系统概述9.2 推理方法9.3 一个简单的专家系统9.4 非确定性推理9.5 专家系统工具9.6 专家系统的应用9.7 专家系统的局限性9.8 本章小结第10章、计算机视觉10.1 计算机视觉概述10.2 数字图像的类型及机内表示10.3 常用计算机视觉模型和关…

计算机重装系统方法教程

​计算机在使用的过程中出现各种问题也是在所难免的&#xff0c;当计算机出现了一些系统故障问题没有办法解决时&#xff0c;或是计算机使用长了以后运行就会变得越来越慢时&#xff0c;这时大家可以考虑通过电脑重装系统来解决&#xff0c;那么&#xff0c;计算机如何重装系统…

ArcGIS基础实验操作100例--实验71多图层叠加查询

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验71 多图层叠加查询 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&am…

MATLAB——PCM编译码实验

目录MATLAB——PCM编译码一、实验原理1.掌握PCM编码原理和译码原理2. 练习使用Matlab编程实现PCM编码和译码3. 了解失真度的概念&#xff0c;能对译码结果进行失真度分析二、实验原理三、实验要求1、用Matlab产生一模拟信号&#xff0c;如&#xff1a; 或者自己编写一信号&…

“微综艺+虚拟场景”,蓝海创意云利用元宇宙技术撬动流量杠杆

1月1日&#xff0c;抖音微综艺节目“友问必答”2023新年直播盛大开幕&#xff0c;蓝海创意云利用vLive虚拟直播系统为此档节目搭建了专属的“元宇宙问答直播间”&#xff0c;整场直播观看人次突破 30W 人次&#xff0c;最高同时在线人数达 3W 人次&#xff0c;独特的直播形式和…

基于Spring+Mybatis框架的人事管理系统源码+数据库,含视频部署教程

人事管理系统 下载地址&#xff1a;基于SpringMybatis框架的人事管理系统源码数据库 部署说明&#xff1a; 项目启动后&#xff0c;在浏览器中访问地址&#xff1a;http://127.0.0.1:8080/personnel/ 由于很多同学反映部署有问题&#xff0c;所以我录了一个视频来演示一下&…

【Python爬虫项目实战】Python爬虫采集某外包平台数据保存本地

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、开发工具二、环境搭建三、数据来源查询分析四、代码实现1. 发送请求2.数据获取3.解析数据4. 保存数据总结前言 今天给大家介绍的是Python爬虫某外包平台数据…

架构设计---搜索引擎的原理

前言&#xff1a; 搜索引擎的倒排索引&#xff0c;数据的搜索与查找技术是计算机软件的核心算法&#xff0c;这方面已有非常多的技术和实践经验。而对于搜索引擎来说&#xff0c;要面对海量的文档进行快速的内容检索、查询的话&#xff0c;最主要的技术是倒排索引技术。 像百…

从0.5到4.0,OceanBase单机分布式一体化的技术演进|DTCC 2022

2022 年 12 月 14 日~16 日&#xff0c;由 IT168 联合旗下 ITPUB、ChinaUnix 两大技术社区主办的第 13 届中国数据库技术大会&#xff08;DTCC 2022&#xff09;在线上隆重召开。大会以“数据智能 价值创新”为主题&#xff0c;上百位技术领袖齐聚云端&#xff0c;进行多维度、…

信道模型:卫星→地面

这里写目录标题比较C. Loo模型&#xff1a;直射阴影&#xff0c;多径不阴影Corazza模型&#xff1a;直射和多径都阴影Lutz模型&#xff1a;好坏2个状态Rayleigh and Rician 信道生成Shadowed-Rician 直射径 散射径[Secure Transmission in Cognitive Satellite Terrestrial Net…

异常流量发现与分析案例

异常现象 NetInside流量分析系统在某教育平台监测过程中&#xff0c;5月14日发现明显的4次流量高峰&#xff08;其中第1-2次产生时间距离较近&#xff09;&#xff0c;详细出现时间如下图。 由上图分析看到&#xff0c;引起流量高峰的IP地址是58.129.247.149&#xff0c;下图…

数字孪生关键技术及其在电力行业应用场景

近年来&#xff0c;我国高度重视数字经济的发展&#xff0c;产业数字化升级战略正在推进中&#xff0c;引导数字经济与实体经济深度融合&#xff0c;促进经济高质量发展。数字孪生作为一项关键技术和提高效能的重要工具&#xff0c;可以有效发挥其在建模、数据采集、分析预测、…

前端组件库自定义主题切换探索-01-方案借鉴与思路参考

探索原因背景 首先自然是项目有需求&#xff0c;这是必须去做的原因 其次&#xff0c;是我们项目没有直接使用市面上现成的基于element-ui或者ant-design的第三方UI框架&#xff0c;比如avue&#xff0c;而是有着自己的UI组件库 第三&#xff0c;我们的组件库基于ant-design-v…

C++ stack和queque

Stack 一.有关stack介绍 stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其只能从容器的一端进行元素的插入、提取或者删除操作。stack是作为容器适配器被实现的&#xff0c;容器适配器即是对特定类封装作为其底层的容器&#xff0c;并…

u盘格式化后数据能恢复吗?当然可以,5步恢复U盘数据

很多人都知道格式化U盘会清空里面的数据&#xff0c;虽然可以进行备份&#xff0c;但是一般我们都不会轻易格式化自己的U盘。但是遇到一些特殊情况&#xff0c;我们必须格式化U盘。u盘格式化后数据能恢复吗&#xff1f;当然可以。 只要你的原始数据没有被覆盖&#xff0c;没有…

新C++(4):模板

"抱紧你的我,比国王富有" C可复用性高&#xff0c;C引入了模板的概念&#xff0c;后面在此基础上&#xff0c;实现了方便开发的标准模板库STL -----前言 一、初始模板 我们先来看看 下面的代码段; 如果此时又有需求&#xff1a; 交换一个char 类型的变量 &#x…

数据库,计算机网络、操作系统刷题笔记29

数据库&#xff0c;计算机网络、操作系统刷题笔记29 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle…

联合证券|主力加仓电气设备、有色金属等行业

上证指数、深证成指早盘探底上升&#xff0c;午后震动回落&#xff0c;尾盘有所上升&#xff1b;创业板指早盘探底冲高&#xff0c;午后震动回落&#xff1b;科创50指数早盘高开高走&#xff0c;午后震动回落。到收盘&#xff0c;上证指数报3157.64点&#xff0c;涨0.08%&#…

如何在Windows中轻松扩大C盘?

因为C盘是系统盘&#xff0c;所以没有足够的空间会导致电脑变慢&#xff0c;影响程序或游戏的运行。新电脑C盘可能有足够的可用空间&#xff0c;但随着对电脑的使用&#xff0c;应用程序安装的越来越多。即便很多程序安装到D盘&#xff0c;但某些程序仍然会占用C盘的部分空间。…