一文搞懂 ThreadLocal

简介

ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用。提供一个线程内公共变量(比如本次请求的用户信息),减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,或者为线程提供一个私有的变量副本,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

数据结构

ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

源码解读

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所 有ThreadLocal对象及其对应的值。

1、set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。

看下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其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

接着看createMap

void createMap(Thread t, T firstValue) {
    //ThreadLocalMap对象赋值给Thread 的 threadLocals 属性
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            //计算在数组中的下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
    }

可以发现这个ThreadLocalMap的key是ThreadLocal类的实例对象,划重点,面试要考的,value为设置的值。

ThreadLocalMap 由一个个 Entry 对象构成 Entry 继承自 WeakReference> ,一个 Entry 由 ThreadLocal 对象和 Object 构 成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该 key就会被垃圾收集器回收。

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

在看ThreadLocalMap的set方法

用数组是因为我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

private void set(ThreadLocal<?> key, Object value) {

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

从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)

然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

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

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

img

到这里,我们就可以理解ThreadLocal究竟是如何工作的了

  • Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
  • 当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
  • ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。

img

2、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();
}

首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。

如何设置一个初始值呢?

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);
    return value;
}

3、remove方法

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

总结下

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

注意的问题

使用不当会发生内存泄漏

img

图中的虚线表示弱引用。

ThreadLocalMap, 这个 map是使用 ThreadLocal 的弱引用作为 Key 的, 弱引用的对象在 GC 时会被回收。

这样, 当把 threadlocal 变量置为 null 以后, 没有任何强引用指向 threadlocal实例, 所以 threadlocal 将会被 gc 回收。 这样一来, ThreadLocalMap 中就会出现key 为 null 的 Entry, 就没有办法访问这些 key 为 null 的 Entry 的 value, 如果当前线程再迟迟不结束的话, 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value, 而这块 value 永远不会被访问到了, 所以存在着内存泄露。

只有当前 thread 结束以后, current thread 就不会存在栈中, 强引用断开,Current Thread、 Map value 将全部被 GC 回收。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("hello");
    ……
} finally {
    localName.remove();
}

ThreadLocal内存泄露原因,如何避免

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)

key 使用强引用

当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法:

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据

  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

使用场景

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

2、线程间数据隔离

3、进行事务操作,用于存储线程事务信息。

4、数据库连接,Session会话管理。

总结

ThreadLocal用法很简单,懂得原理才能避免一些问题出现。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

未授权访问-api接口

特别注意api接口的一些命名规则 常见的是这种&#xff0c;具体要看开发人员怎么命名的 而确认api路径的最好办法还是去多出发几个功能点&#xff0c;看他的路径&#xff0c;比如下面触发多个功能点 对比得知两个路径都有pyr/user/这时候可能就会觉得这就是api路径&#xff0c;但…

Azure的VFP和虚拟IP地址

Azure 的Virtual filtering platform (VFP) 是Azure 网络地址转换,端口转换和端口分配的基础。 下面我们来深入介绍一下VFP的工作方式。 VFP的出站动作。 对于客户端地址作为虚拟IP的出站目的地址的时候,VFP 驱动会负责做以下两个动作。 源地址转换。端口地址转换。VFP 和 S…

一分钟了解mos管选型

在选择MOS管时&#xff0c;需要考虑多个关键参数以确保选用的MOS管能够满足特定应用的需求。下面是一些主要参数的介绍 额定电压&#xff08;Vds&#xff09; 也称为漏源电压&#xff0c;通常我们所说的耐压&#xff0c;是指MOS管能够承受的最大电压差。 在选择MOS管时&#xf…

数据湖概述:大数据演进阶段-数据湖

文章目录 一. 大数据发展过程1. 离线大数据平台2. Lambda架构&#xff1a;速度层批层3. Kappa架构&#xff1a;流批一体4. 大数据架构痛点总结 二. 数据湖助力于解决数据仓库痛点问题1. 数据湖特点2. 开源数据湖的架构 三. 数据湖和数据仓库理念的对比1. 数据湖和数据仓库对比2…

c++的STL(7) -- stack

stack容器概述 stack容器其实是实现了一种和栈相同结构的容器。 如图&#xff0c;栈这种结构有两端: 栈底和栈顶。 特殊之处在于&#xff0c;这种结构&#xff0c;我们对数据的操作(删除数据&#xff0c;修改数据&#xff0c;查询数据&#xff0c;添加数据)只能在一端进行(栈…

TAB标签美化 - SVG作为mask

今天觉得V3的标签不是很好看&#xff0c;忽然想起来之前看过Vue Admin Beautiful Pro的样式挺好的&#xff0c;顺手研究了一把。发现Vue Admin Beautiful是采用PNGmask css来解决的。于是乎打算把V3的标签页做点小美化&#xff0c;但是迁移过程发生些小插曲&#xff0c;在此记录…

第1讲——预备知识

一、视觉SLAM十四讲在讲些啥 SLAM&#xff1a;Simultaneous Localization and Mapping 翻译&#xff1a;同时定位与地图构建 搭载特定传感器的主体&#xff0c;在没有环境先验信息的情况下&#xff0c;于运动过程中建立环境的模型&#xff0c;同时估计自己的运动。 当特定传感…

Solidity入门1: 3. 函数类型

Solidity中的函数 solidity官方文档里把函数归到数值类型 函数结构 function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)] 看着些复杂&#xff0c;咱们从前往后一个一个看&…

【MySQL】:深入解析多表查询(上)

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; MySQL从入门到进阶 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. 多表关系1.1 一对多1.2 多对多1.3 一对一 二. 多表查询概述2.1 概述2.2 分类…

Docker Desktop 不支持 host 网络模式

先把这个结论的放在前面&#xff0c;直接访问链接就能看到官方文档中已经明确说了不支持。 参考链接&#xff1a;docker desktop for windows 不支持 host 网络模式 以前对于 docker 的网络模式&#xff0c;一直只是了解&#xff0c;没有亲自尝试过。结果今天在尝试 docker 的 …

『大模型笔记』LLMs入门:从头理解与编码LLM的自注意力机制

LLMs入门&#xff1a;从头理解与编码LLM的自注意力机制 这里直接引用我语雀上的的文章&#xff1a;《从头理解与编码LLM的自注意力机制》

科东软件参加广州机器人产业联盟举办先进工业母机专家研讨会

工业母机是“制造机器的机器”&#xff0c;具有基础性、通用性、战略性特征&#xff0c;包括了减材切削机床、等材成形装备、增材制造装备及其控制系统等&#xff0c;是衡量国家工业水平和竞争力的重要标志。广东省作为全球知名的制造业基地&#xff0c;非常重视高端装备领域工…

python 笔记

文章目录 pdbpdb开始调试pythonpdb设置断点单步执行进入到函数的内部执行到下一个断点或程序结束调用栈查看命令查看当前函数调用堆栈向上一层函数查看调用堆栈查看源代码 importimport 用法 numpy导入numpy模块numpy常用函数np.argmaxnp.sum range生成连续序列生成不连续序列 …

【随笔】Git 高级篇 -- 撤销变更(十四)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

如何使用 Midjourney?2024年最新更新

一&#xff1a;基础篇 1&#xff1a;注册 首先&#xff0c;你需要注册一个 Discord 账号&#xff0c;然后加入 Midjourney 的 Discord 服务器。或者去 Midjourney 的官网点击右下角的 Join the Beta&#xff1a; ​ 2&#xff1a;在 Discord 公共服务器里使用 注册并进入到…

一、Docker部署GitLab(详细步骤)

Docker部署GitLab&#xff08;详细步骤&#xff09; 一、拉取镜像二、启动容器三、修改配置四、修改密码五、浏览器访问 一、拉取镜像 docker安装教程&#xff1a;https://qingsi.blog.csdn.net/article/details/131270071 docker pull gitlab/gitlab-ce:latest二、启动容器 …

MySQL数据库 数据库基本操作(三):表的增删查改(中)

1. 数据库的约束 1.1 约束类型(一般发生于表的创建中) NOT NULL - 指示某列不能存储 NULL 值。UNIQUE - 保证某列的每行必须有唯一的值。DEFAULT - 规定没有给列赋值时的默认值。PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列&#xff08;或两个列多个列的结合&#xf…

一站式自动化测试平台-Autotestplat

3.1 自动化平台开发方案 3.1.1 功能需求 3.1.3 开发时间计划 如果是刚入门、但有一点代码基础的测试人员&#xff0c;大概 3 个月能做出演示版(Demo)进行自动化测试&#xff0c;6 个月内胜任开展工作中项目的自动化测试。 如果是有自动化测试基础的测试人员&#xff0c;大概 …

突破!AI机器人拥有嗅觉!仿生嗅觉芯片研究登上Nature子刊

我们一直梦想着让AI与人类能够更加相似&#xff0c;赋予它们视觉与听觉。而让机器人拥有嗅觉一直以来面临着巨大的困难。 香港科技大学范志勇教授领导的研究团队凭借最新研发的仿生嗅觉芯片&#xff08;BOC&#xff09;在这一领域取得了重大突破。该研究成果目前已被发表到IF …