ThreadLocal详解

一、什么是ThreadLocal

1、什么是ThreadLocal&为什么用ThreadLocal

ThreadLocal,即线程本地变量,在类定义中的注释如此写This class provides thread-local variables。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是在操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了并发场景下的线程安全问题。属于空间换时间的解决线程安全问题的方案。

2、ThreadLocal的使用场景

  • 在某些项目中,日志需要存储用户的信息,因此在切面中,可以使用ThreadLocal存储用户信息,或者使用ThreadLocal存储各个接口的返回结果,在切面中统一处理
  • 在格式化日期的时候,用到SimpleDateFormat,需要使用Thread Local来保证线程安全,如下
public class test {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    System.out.println(sdf.parse("2023-03-17 10:34:30"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这里插入图片描述
出现了线程安全问题,接下来,我们使用ThreadLocal处理

public class test {
    private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                try {
                    System.out.println(sdf.get().parse("2023-03-17 10:34:30"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这里插入图片描述
这是因为SimpleDateFormat是一个线程不安全类,在多线程情况下会出现问题,而通过ThreadLocal处理后,变成了每个线程私有的一个类,因此成功运行。

二、ThreadLocal的原理

1、关键代码分析

先来看下Thread类中与ThreadLocal有关的代码和ThreadLocal的关键代码

class Thread{
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

从代码中可以看到,ThreadLocal.ThreadLocalMap是Thread的一个属性,也就是说,一个线程持有一个ThreadLocal.ThreadLocalMap对象。

class ThreadLocal{
	//ThreadLocal的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);
    }
    //ThreadLocal的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();
    }
    //ThreadLocalMap类
    static class ThreadLocalMap{
         private Entry[] table;
         static class Entry extends WeakReference<ThreadLocal<?>>{
             Object value;
             Entry(ThreadLocal<?>k, Object v){
                 super(k);
                 value = v;
             }
         }
    }
    //获取map方法
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

ThreadLocalMap是ThreadLocal类的静态内部类,ThreadLocalMap内部维护了Entry数组,每个Entry是一个完整的key-value对象,其中key为ThreadLocal本身。接着看ThreadLocal的get/set方法,都是先获取当前线程对象t,然后通过getMap方法返回当前线程对象t的threadLocals对象,也就是线程持有的ThreadLocalMap对象,然后将当前线程对象作为key传入,从ThreadLocalMap对象中获取值或者设置值。综上,我们可以做出一个结构图。

2、ThreadLocal和Thread结构图

在这里插入图片描述

3、ThreadLocal原理概述

综合上面的代码分析和结构图,我们可以得出ThreadLocal的基本原理

Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。

三、引用问题&内存泄漏

1、Java的引用类型

  • 强引用:我们常用的new对象就是强引用,Object obj = new Object();这种引用对象在内存不足时,jvm宁愿抛出oom错误也不会被回收
  • 软引用:在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:无论内存是否足够,只要jvm开始进行垃圾回收,那些被弱引用关联的对象都会被回收
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
/**
 * 测试引用类型,jvm参数设置为-Xms10M -Xmx10M
 */
public class test {
    private static List<Object>list = new ArrayList<>();
    public static void main(String[] args) {
        testWeakReference();
    }

    /**
     * 强引用
     * 直接报错OOM
     */
    public static void testStrongReference(){
        byte[] bytes = new byte[1024*1024*11];
    }

    /**
     * 软引用,当发生gc时,内存不存会回收对象
     * null
     * null
     * null
     * null
     * [B@7eda2dbb
     */
    public static void testSoftReference(){
        for (int i = 0; i < 5; i++) {
            byte[] bytes = new byte[1024*1024*5];
            SoftReference<byte[]> softReference = new SoftReference<>(bytes);
            list.add(softReference);
        }
        System.gc();
        for (int i = 0; i < list.size(); i++) {
            Object o = ((SoftReference)list.get(i)).get();
            System.out.println(o);
        }
    }

    /**
     * 弱引用,发生gc时,无论内存是否足够,都回收对象
     * null
     * null
     * null
     * null
     * null
     */
    public static void testWeakReference(){
        for (int i = 0; i < 5; i++) {
            byte[] bytes = new byte[1024*1024*5];
            WeakReference<byte[]> weakReference = new WeakReference<>(bytes);
            list.add(weakReference);
        }
        System.gc();
        for (int i = 0; i < list.size(); i++) {
            Object o = ((WeakReference)list.get(i)).get();
            System.out.println(o);
        }
    }
}

2、为什么ThreadLocalMap的Entry对象的key用的是弱引用

如下图所示,Entry对象的key继承了弱引用的ThreadLocal
在这里插入图片描述
我们先看下这个ThreadLocal的引用图
在这里插入图片描述
我们先假设用的是强引用,只要我们的线程一直存活(或者使用了线程池),那么,无论ThreadLocal变量的引用存在与否,ThreadLocal对象都会被entry对象引用,那么就造成了即使ThreadLocal变量的引用不存在了,这个ThreadLocal对象也不会被回收,造成内存泄漏;而设置成弱引用,那么发生gc,检查到ThreadLocal对象只存在弱引用,就会被回收。

3、内存泄漏

先来看一段代码及其两种演示结果

/**
 * jvm设置为最大20m
 * 如果不调用remove方法,运行过程中出现oom错误
 * 执行remove方法,不会出现oom错误
 */
public class test {
    static class zyObject{
        private byte[] bytes = new byte[10 * 1024 * 1024];
    }

    private static ThreadLocal<zyObject> zyObjectThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
        for (int i = 0; i < 10; i++) {
            int a = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第"+a+"个线程");
                    zyObject zyObject = new zyObject();
                    zyObjectThreadLocal.set(zyObject);
                    zyObject = null;//将对象设置为 null,表示此对象不在使用了
//                    zyObjectThreadLocal.remove();
                }
            });
            Thread.sleep(1000);
        }
    }
}

在这里插入图片描述
在这里插入图片描述

上面说到,Entry对象的key使用弱引用的ThreadLocal对象,发生GC时会被回收,那么,还存在什么内存泄漏问题呢?这是因为ThreadLocal回收的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收,造成内存泄漏。所以我们使用完ThreadLocal之后,要记得调用remove方法。实际上,ThreadLocal在设计的时候也考虑过这些问题,在set、get、remove方法中都有对key为null的移除。

四、如何使用ThreadLocal父子线程传值

我们知道,ThreadLocal作为本地线程变量,它的变量是私有的,那么如何进行线程间的传值呢?先来看下面这段代码

/**
 * 运行结果
 * null
 * 我是BBBBBBBBBBBBBBB
 */
public class test {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
        threadLocal.set("我是AAAAAAAAAAAAAAAAA");
        inheritableThreadLocal.set("我是BBBBBBBBBBBBBBB");
        new Thread(()->{
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        }).start();
    }
}

可以看到,InheritableThreadLocal实现了父子线程间的传值。我们看下Thread类的初始化代码

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) { 
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
    }

可以发现,当parent的inheritableThreadLocals不为null时,就会将parent的inheritableThreadLocals,赋值给前线程的inheritableThreadLocals。说白了,就是如果当前线程的inheritableThreadLocals不为null,就从父线程哪里拷贝过来一个过来,类似于另外一个ThreadLocal,数据从父线程那里来的。

总结

这篇文章主要从ThreadLocal的结构、原理、常见问题方面阐述了ThreadLocal的相关知识,并没有去讲解ThreadLocalMap的初始化、扩容、重新hash等知识,这些内容在日常使用中不算特别重要,如果需要了解,适当看一下源码及Map的原理即可。

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

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

相关文章

C++基础算法④——排序算法(插入、桶附完整代码)

排序算法 1.插入排序 2.桶排序 1.插入排序 基本思想&#xff1a;将初始数据分为有序部分和无序部分&#xff1b;每一步将无序部分的第一个值插入到前面已经排好序的有序部分中&#xff0c;直到插完所有元素为止。步骤如下&#xff1a; 每次从无序部分中取出第一个值&#x…

图像分类卷积神经网络模型综述

图像分类卷积神经网络模型综述遇到问题 图像分类&#xff1a;核心任务是从给定的分类集合中给图像分配一个标签任务。 输入&#xff1a;图片 输出&#xff1a;类别。 数据集MNIST数据集 MNIST数据集是用来识别手写数字&#xff0c;由0~9共10类别组成。 从MNIST数据集的SD-1和…

在Clion开发工具上使用NDK编译可以在安卓上执行的程序

1. 前言 因为工作需要&#xff0c;我要将一份C语言代码编译成可执行文件传送到某安卓系统里执行。 众所周知&#xff0c;使用ndk编译代码有三种使用方式&#xff0c;分别是基于 Make 的 ndk-build、CMake以及独立工具链。以前进行ndk编程都是使用ndk-build进行的&#xff0c;新…

RocketMQ的基本概念、系统架构、单机安装与启动

RocketMQ的基本概念、系统架构、单机安装与启动 文章目录RocketMQ的基本概念、系统架构、单机安装与启动一、基本概念1、消息&#xff08;Message&#xff09;2、主题&#xff08;Topic&#xff09;3、标签&#xff08;Tag&#xff09;4、队列&#xff08;Queue&#xff09;5、…

C# 教你如何终止Task线程

我们在多线程中通常使用一个bool IsExit类似的代码来控制是否线程的运行与终止&#xff0c;其实使用CancellationTokenSource来进行控制更为好用&#xff0c;下面我们将介绍CancellationTokenSource相关用法。C# 使用 CancellationTokenSource 终止线程使用CancellationTokenSo…

【Leetcode】-有效的括号

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 文章目录前言前言 今天我们再来讲一期关于题目的博客&#xff0c;我挑选的是一道leet…

Git学习与gitlab中央仓库搭建(详细介绍)

环境&#xff1a;centos7.3一&#xff0c;Git的发展史git&#xff1a;分布式版本控制系统&#xff0c;是当前最流行的版本控制软件创始人&#xff1a;林纳斯.拖瓦兹二&#xff0c;部署Git环境1.安装git服务[rootlocalhost ~]# yum -y install git2.配置git环境不一定是data目录…

【C++】初识模板

放在专栏【C知识总结】&#xff0c;会持续更新&#xff0c;期待支持&#x1f339;前言在谈及本章之前&#xff0c;我们先来聊一聊别的。橡皮泥大家小时候应该都玩过吧&#xff0c;通常我们买来的橡皮泥里面都会带有一些小动物的图案的模子。我们把橡皮泥往上面按压&#xff0c;…

【性能分析】分析JVM出现的内存泄漏的性能故障

分析JVM出现的内存持续增加的性能故障手册 前言 本文通过常见的性能文件为例&#xff0c;提供简单清晰的思路去快速定位问题根源&#xff0c;从而可以快速解决性能故障。 性能问题介绍 在性能测试工作中针对Java程序最重要的是要关注JVM的内存消耗情况&#xff0c;JVM的内存…

面试错题本

目录2023.3.21 深信服哈夫曼树哈夫曼编码2023.3.21 深信服 ​同一线程共享的有堆、全局变量、静态变量、指针&#xff0c;引用、文件等&#xff0c;而独自占有栈 友元函数不能被继承&#xff0c;友元函数不是成员函数 友元函数不能被继承&#xff0c;友元函数不是当前类的成员…

Vue2项目总结-电商后台管理系统

Vue2项目总结-电商后台管理系统 去年做的项目&#xff0c;拖了很久&#xff0c;总算是打起精力去做这个项目的总结&#xff0c;并对Vue2的相关知识进行回顾与复习 各个功能模块如果有过多重复冗杂的部分&#xff0c;将会抽取部分值得记录复习的地方进行记录 一&#xff1a;项目…

精心整理前端主流框架学习路径

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 前端主流框架 前端框架指的是用于构建Web前端应用程序的框架&#xff0c;使用框架进行前端开发带来以下显著优势&#xff1a; 提高开发效率&#xff1a;前端框架提供了现成的…

STM32的CAN总线调试经验分享

相关文章 CAN总线简易入门教程 CAN总线显性电平和隐性电平详解 STM32的CAN总线调试经验分享 文章目录相关文章背景CAN总线CAN控制器CAN收发器调试过程硬件排查CAN分析仪芯片CAN控制器调试总结背景 最近负责的一个项目用的主控芯片是STM32F407IGT6&#xff0c;需要和几个电机控…

DWF文件怎么用CAD打开?DWF输入CAD步骤

DWF是一种开放、安全的文件格式&#xff0c;它可以将丰富的设计数据高效率地分发给需要查看、评审或打印这些数据的任何人。那么&#xff0c;DWF文件如何打开呢&#xff1f;下面就和小编一起来了解一下DWF输入浩辰CAD软件中的具体操作步骤吧&#xff01; DWF输入CAD中步骤&…

安装CentOS系统

打开 Oracle VM VirtualBox 点击新建 输入名称 点击下一步 点击下一步 点击创建 点击下一步 点击下一步 分配30G硬盘 点击创建 创建成功 点击启动按钮 选择 CentOS 系统 iso 镜像文件 点击启动 按键盘方向键 “上键”&#xff0c;选择第一项 按键盘回车键&#xff0c;然后等待 …

QT搭建MQTT开发环境

QT搭建MQTT开发环境 第一步、明确安装的QT版本 注意&#xff1a; 从QT5.15.0版本开始&#xff0c;官方不再提供离线版安装包&#xff0c;除非你充钱买商业版。 而在这里我使用的QT版本为5.15.2&#xff0c;在线安装了好久才弄好&#xff0c;还是建议使用离线安装的版本 在这里…

代码随想录复习——单调栈篇 每日温度 下一个更大元素12 接雨水 柱状图中最大的矩形

739.每日温度 每日温度 暴力解法双指针 def dailyTemperatures(self, temperatures: List[int]) -> List[int]:n len(temperatures)res [0] * nfor i in range(n):for j in range(i,n):if temperatures[j] < temperatures[i]: continueelse: res[i] j-ibreakreturn …

pytorch 计算混淆矩阵

混淆矩阵是评估模型结果的一种指标 用来判断分类模型的好坏 预测对了 为对角线 还可以通过矩阵的上下角发现哪些容易出错 从这个 矩阵出发 可以得到 acc &#xff01; precision recall 特异度&#xff1f; 目标检测01笔记AP mAP recall precision是什么 查全率是什么 查准率…

【K8S系列】深入解析Pod对象(一)

目录 序言 1.问题引入 1.1 问题描述 2 问题解答 2.1 pod 属性 2.1.1 NodeSelector 2.1.2 HostAliases 2.1.3 shareProcessNamespace 2.1.4 NodeName 2.1.5 其他pod属性 2.2 容器属性 2.2.1 ImagePullPolicy 2.2.2 Lifecycle 3 总结 4. 投票 序言 任何一件事情&am…

一文读懂强化学习!

一.了解强化学习1.1基本概念强化学习是考虑智能体&#xff08;Agent&#xff09;与环境&#xff08;Environment&#xff09;的交互问题&#xff1a;智能体处在一个环境中&#xff0c;每个状态为智能体对当前环境的感知&#xff1b;智能体只能通过动作来影响环境&#xff0c;当…