ThreadLocal使用和原理

news2024/11/20 15:28:10

ThreadLocal是线程本地变量,用来解决并发下数据隔离性的问题,不能解决共享。

他可以将一个变量拷贝的线程内,线程调用时再线程内进行使用,相当于给每个线程复制一个副本供各个线程使用。

ThreadLocal简单使用

他的目的很简单,就是让每个线程操控同一个变量做不同的事情,互不干扰

我们这里使用栈进行对比,用来查看高并发下的问题

@Slf4j
public class TestThreadLocal {
    public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();
        Deque<String> stack = new ArrayDeque<>();
        for (int i = 0; i < 10; i++) {
            final int j = i;
            new Thread(()->{
                String s = Thread.currentThread().getName()+"::"+j;
                local.set(s);
                stack.push(s);
                log.debug("local--->{}",local.get());
                //解决内存泄露问题
                local.remove();
                if(!stack.isEmpty()){
                    log.debug("stack-->{}",stack.pop());
                }else{
                    log.debug("null---");
                }

            },"t"+i).start();
        }
    }
}

运行结果:

# 这里使用local没出现问题,每个线程打印的都是自己存入的数据
15:03:56.862 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t8::8
15:03:56.862 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t6::6
15:03:56.862 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t9::9
15:03:56.862 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t1::1
15:03:56.862 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t7::7
# 这里就已经出问题了,t9线程打印了t6线程存的数据
15:03:56.866 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t6::6
15:03:56.866 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t9::9
15:03:56.866 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t7::7
15:03:56.862 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t2::2
15:03:56.862 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t4::4
15:03:56.866 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t8::8
15:03:56.862 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t3::3
15:03:56.866 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t3::3
15:03:56.862 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t5::5
15:03:56.866 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t2::2
15:03:56.866 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t1::1
15:03:56.862 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t0::0
15:03:56.866 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t5::5
15:03:56.866 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t4::4
15:03:56.866 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t0::0

上面的例子应该已经帮我们很好的理解了ThreadLocal的作用,以及我们使用其他存储的对比,接下来我们看看ThreadLocal内部是怎么实现的

原理

看原理之前我们先了解一下ThreadLocal的存储结构,以及Thread、ThreadLocal、ThreadLocalMap之间的关系:

image-20230518165716680

我们看Thread的属性

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

Thread内部属性默认是有ThreadLocalMap的,不过默认为null罢了,而ThreadLocalMap中存储的键值结构是<ThreadLocal,value>,所以实际可以用多个ThreadLocal存取不同的值

ThreadLocal结构

class ThreadLocal{
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //...
        }
    }
    //....
}

每个ThreadLocal内有一个ThreadLocalMap,ThreadLocalMap类似HashMap,但是也有不同,其中包含一个Entry,类似hashmap的Entry,不过有区别,下面会详细说。

我们使用ThreadLocal无非就那几个方法,所以我们直接挑重点看

  1. set
public void set(T value) {
    //获取到当前调用的线程
    Thread t = Thread.currentThread();
    //查看当前线程是否已经实例化过ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果已经实例化过,那么就将当前ThreadLocal作为key,value作为值加入map
    if (map != null)
        map.set(this, value);
    else
        //否则创建ThreadLocalMap
        createMap(t, value);
}
  • getMap,用来返回线程的ThreadLocalMap
 	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • createMap
 	void createMap(Thread t, T firstValue) {
        //实例化当前ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

如果该线程的ThreadLocalMap还没被创建,那么在这将会实例化一次,并且该线程的ThreadLocalMap以后会有永远拥有,另一个ThreadLocal来存储的时候会直接使用该ThreadLocalMap

  • 实例化ThreadLocalMap
		//可以看到 local会是key,value作为值
		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内包含了一个Entry数组,用于存放不同的键值

所以这里set方法的流程是:

  • 获取当前线程对象

  • 查看该线程内的ThreadLocalMap是否被实例化过

    • 如果没有被实例化,那么进行实例化,并将当前ThreadLocal和值存进去

    • 如果被实例化过,那么将当前ThreadLocal找个合适的位置存入,map.set(this, value);

    • 那么我们看这个方法:map.set(this, value)

		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.
			//获取当前map内的entry数组
            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();
				
                //如果当前ThreadLocal已经存入值,那么进行覆盖并返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                
				//如果当前值过期,也就是弱引用失效,将进行替换,将当前key替换进去,这里不展开讲
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			
            //到这说明当前ThreadLocal没有被放入过,所以这里直接new
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //解决hash冲突,如果ThreadLocal有很多的情况下
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

如果当前线程的ThreadLocalMap已经存在了,再次加入ThreadLocal和value无非两种情况,之前加入过和没加入过,加入过的找到并替换,没加入的直接new一个新的对象出来

到这里set方法基本结束

接下来看get方法

  1. get
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //被实例化过
        if (map != null) {
            //找当前ThreadLocal存放的位置
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果已经找到,直接将值返回,没找到说明以前就没存过该值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //没实例化过先实例化,然后将null设置进去,返回null
        return setInitialValue();
    }
  • setInitialValue
	private T setInitialValue() {
        T value = initialValue();  //initialValue()返回值为null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

接下来我们看怎么找到的该ThreadLocal对应的值

  • getEntry
 		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);
        }
  • getEntryAfterMiss(key, i, e);
	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)
                    //如果为null,往后寻找一段,找到脏entry进行清理,直到找到null为止
                    expungeStaleEntry(i);
                else
                    //开发地址法找i下标
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

以上是ThreadLocal常用方法的原理

差别

前文我们说了,ThreadLocalMap和HashMap类似,但是也有差别:

  • HashMap实现方式为数组+链表
    • Entry内保存key和value值,是强引用关系
    • 采用拉链法寻值
  • ThreadLocalMap实现方式为数组
    • Entry仅仅存储了value值
    • 采用开放地址法寻值,当前位置有hash冲突时就会一直往下找,直到找到可用的为止

内存泄漏

我们知道ThreadLocal是弱引用关系,那么当GC之后ThreadLocal被清理掉后,value还没被清理掉,此时会造成内存泄露。

之所以是弱引用,是因为每个Entry的key虽然是ThreadLocal,但是并不是直接指向,Entry内没有让ThreadLocal存储的位置,都在Entry的父类里

解决内存泄露的方法也很简单,就是在使用完之后手动remove就可以了

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

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

相关文章

Unity用AI制作天空盒,并使用,详细图文教程

Unity用AI制作天空盒&#xff0c;并使用&#xff0c;详细图文教程 效果AI制作使用总结版权声明 效果 先上我自己做的效果 AI制作 首先登录AI制作的网站&#xff0c;打开就可以用&#xff0c;不需要登录 这是网址&#xff1a;https://skybox.blockadelabs.com/ 1.创建新的 2…

idea操作——如何format代码

1.选中需要format的类&#xff0c;然后右击&#xff0c;选择reformat code 2.出现的复选框根据自己的需求进行选择。然后点击OK即可。 Optimize imports 优化导入 选中此复选框可从所选范围内的代码中删除未使用的导入语句。 删除代码中没使用到的import 。使导入最优化 Rearr…

【C++】-模板初阶(函数和类模板)

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 文章目录 前言一、为什么要模板&…

【面试题】谈谈你对vite的了解

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 1.什么是vite vite是新一代前端构建工具&#xff0c;能够显著提升前端开发体验。他是使用…

东邻到家小程序|东邻到家小程序源码|东邻到家小程序开发功能

上门服务这几年已经越来越火爆&#xff0c;不论是家政、按摩、美甲等等都在不断的发展上门服务&#xff0c;这几年东邻到家小程序系统在不断的摸索阶段&#xff0c;对于系统各方面的需求也在不断提升&#xff0c;东郊到家小程序通过线上匹配用户和技师的需求&#xff0c;让人们…

低代码开发打破CRM开发瓶颈,是否靠谱呢?

低代码开发平台是一种快速开发应用程序的新兴技术&#xff0c;它通过提供可视化开发工具和预配置组件&#xff0c;使开发者更加高效地创建应用程序。低代码开发平台的出现为企业开发带来了一次全新的机遇&#xff0c;尤其是在CRM领域。但是&#xff0c;低代码开发在CRM领域中是…

得物前端巡检平台的建设和应用(建设篇)

1.背景 我们所在的效能团队&#xff0c;对这个需求最原始的来源是在一次“小项目”的评审中&#xff0c;增长的业务同学提出来的&#xff0c;目的在于保障前端页面稳定性的同时减少大量测试人力的回归成本。 页面稳定性提升&#xff0c;之前迭代遇见过一些C端的线上问题&…

自学软件测试,我还是劝你算了吧。。。

本人8年测试经验&#xff0c;在学测试之前对电脑的认知也就只限于上个网&#xff0c;玩个办公软件。这里不能跑题&#xff0c;我为啥说&#xff1a;自学软件测试&#xff0c;一般人我还是劝你算了吧&#xff1f;因为我就是那个一般人&#xff01; 软件测试基础真的很简单&…

乒乓测评:电视盒子哪个牌子最好?2023电视盒子品牌排行榜

这里是乒乓测评&#xff0c;致力于带来更客观、真实的数码产品体验。本期我们测评的主题是电视盒子哪个牌子最好&#xff0c;为此我们购入了二十多款热门电视盒子&#xff0c;从硬件配置、视频流畅度、系统界面、操作、广告程度等方面进行多维度的测评&#xff0c;根据结果整理…

C++每日一练:详解-买铅笔影分身三而竭

文章目录 前言一、买铅笔二、影分身三、三而竭总结 前言 这回又换成C了&#xff0c;Python要用C也要用&#xff0c;没有哪个正经程序员只会一门语言的&#xff0c;咱可是CSDN认证带V的全栈攻城狮。今天的题目除了买铅笔都还是有点难度的&#xff0c;虽然影分身主要是考验阅读理…

【matlab报错】:函数或变量 ‘randint‘ 无法识别。

问题产生 首先定位问题&#xff0c;这个问题是由matlab版本造成的&#xff0c;随着matlab版本的更新&#xff0c;matlab删除了 randint 这个函数。 怎么替代呢&#xff1f;鼠标悬浮在报错代码上面&#xff0c;如下&#xff1a; matlab提示我们对代码进行相应更改后改用randi了…

基于SSM+JSP校园二手交易系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

5年测试经验华为社招:半月3次面试,成功拿到Offer

背景经历 当时我工作近5年&#xff0c;明显感觉到了瓶颈期。具体来说&#xff0c;感觉自己用过很多测试框架和测试工具、做过一些测试开发、也有过高并发的性能测试&#xff0c;但是从技术深度上感觉不足&#xff0c;到后期时做事也没有明显挑战&#xff0c;完全适应了公司节奏…

新的网络钓鱼即服务平台让网络犯罪分子生成令人信服的网络钓鱼页面

至少从2022年中期开始&#xff0c;网络犯罪分子就利用一个名为“伟大”的新型网络钓鱼即服务(PhaaS或PaaS)平台来攻击微软365云服务的企业用户&#xff0c;有效地降低了网络钓鱼攻击的门槛。 思科Talos研究员蒂亚戈佩雷拉表示:“目前&#xff0c;Greatness只专注于微软365钓鱼…

[Hadoop]大数据导论与Linux基础

目录 大数据导论 企业数据分析方向 数据分析基本步骤 大数据时代 分布式与集群 Linux操作系统概述 操作系统概念与分类 Linux起源与发展 Linux内核与发行版本 VMware Workstation虚拟机使用 VMware虚拟机概念 VMware虚拟机常规使用 Linux常用基础命令 Linux文件系…

Spring Boot单元测试

什么是单元测试&#xff1f; 单元测试(unit testing)&#xff0c;是指对软件中的最小可测试单元进行检查和验证的过程就叫单元测试。 单元测试是开发人员编写的一小段代码&#xff0c;用于检验被测代码的一个很小的、很明确的(代码) 功能是否正确。执行单元测试就是为了证明某…

Java面试知识点(全)- Java并发- Java并发基础一

Java面试知识点(全) 导航&#xff1a; https://nanxiang.blog.csdn.net/article/details/130640392 注&#xff1a;随时更新 多线程解决什么问题 CPU、内存、I/O 设备的速度是有极大差异的&#xff0c;为了合理利用 CPU 的高性能&#xff0c;平衡这三者的速度差异&#xff0c…

PMP课堂模拟题目及解析(第11期)

101. 一家咨询公司的负责人启动一个项目来扩大公司提供的服务数量&#xff0c;这公司具有竞争优势、出色的企业知识以及卓越的声誉&#xff0c;高管团队担心与增加新服务相关的负面业务结果的可能性。若要评估负面业务结果的可能性和影响&#xff0c;项目经理应该使用什么&…

matlab写入txt文件进行自动化测试总结:fopen、fclose和fprintf的用法

前言 日常学习的过程中使用了matlab读写txt文件&#xff0c;记录一下基本函数的使用&#xff0c;本文主要介绍了fopen、fclose和fprintf几个函数&#xff0c;这些主要是面向txt格式的文件保存数据。还有其他几个函数&#xff0c;比如fread和fwrite&#xff0c;用过但是他们是针…

【dcdc】AP2813 DCDC降压恒流芯片 两路输出 一路恒流 一路瀑闪 电动摩托汽车灯方案

1&#xff0c;方案来源&#xff1a;深圳市世微半导体有限公司 汤巧 2&#xff0c;产品描述 AP2813 是一款双路降压恒流驱动器,高效率、外围简单、内置功率管&#xff0c;适用于 5-80V 输入的高精度降压 LED 恒流驱动芯片。内置功率管输出最大功率可达12W&#xff0c;最大电流…