Java笔记:ThreadLocal

news2024/12/23 17:52:37

1. ThreadLocal简介

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示

ThreadLocal类中提供了几个方法:

1.public T get() { }
2.public void set(T value) { }
3.public void remove() { }
4.protected T initialValue(){ }

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

2. ThreadLocal的应用场景?

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。在下面会例举几个场景。

3. ThreadLocal简单使用

下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

public class ThreadLocalTest {

    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) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

结果

thread1 :localVar1
thread2 :localVar2
after remove : null
after remove : null

4. ThreadLocal的实现原理

下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null

只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。除此之外,和我所想的不同的是,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面(前面也说过,该变量是Thread类的变量)。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。下面我们通过查看ThreadLocal的set、get以及remove方法来查看ThreadLocal具体实怎样工作的

1、set方法源码

public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}

在上面的代码中,(2)处调用getMap方法获得当前线程对应的threadLocals(参照上面的图示和文字说明),该方法代码如下

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
}

如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals,该方法如下所示

 void createMap(Thread t, T firstValue) {
     t.threadLocals = new ThreadLocalMap(this, firstValue);
 }

createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。

2、get方法源码
  在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}

3、remove方法的实现
remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量

public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

4、如下图所示:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似于一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值。每个线程的本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。

5. ThreadLocal使用有哪些坑及注意事项

我经常在网上看到骇人听闻的标题,ThreadLocal导致内存泄漏,这通常让一些刚开始对ThreadLocal理解不透彻的开发者,不敢贸然使用。越不用,越陌生。这样就让我们错失了更好的实现方案,所以敢于引入新技术,敢于踩坑,才能不断进步。
我们来看下为什么说ThreadLocal会引起内存泄漏,什么场景下会导致内存泄漏?

先回顾下什么叫内存泄漏,对应的什么叫内存溢出
①Memory overflow:内存溢出,没有足够的内存提供申请者使用。
②Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
显然是TreadLocal在不规范使用的情况下导致了内存没有释放。

红框里我们看到了一个特殊的类WeakReference,同样这个类,应用开发者也同样很少使用,这里简单介绍下吧

类型回收时间应用场景
强引用一直存活,除非GC Roots不可达所有程序的场景,基本对象,自定义对象等
软引用内存不足时会被回收一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存
弱引用只能存活到下一次GC前生命周期很短的对象,例如ThreadLocal中的Key。
虚引用随时会被回收, 创建了可能很快就会被回收可能被JVM团队内部用来跟踪JVM的垃圾回收活动

既然WeakReference在下一次gc即将被回收,那么我们的程序为什么没有出问题呢?

①所以我们测试下弱引用的回收机制:

这一种存在强引用不会被回收。

这里没有强引用将会被回收。
上面演示了弱引用的回收情况,下面我们看下ThreadLocal的弱引用回收情况。

②ThreadLocal的弱引用回收情况

如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
Threadref–>Thread–>ThreadLocalMap–>Entry,所以这将导致内存泄漏。
下面我们模拟复现ThreadLocal导致内存泄漏:
1.为了效果更佳明显我们将我们的treadlocals的存储值value设置为1万字符串的列表:

class ThreadLocalMemory {
    // Thread local variable containing each thread's ID
    public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
        @Override
        protected List<Object> initialValue() {
            List<Object> list = new ArrayList<Object>();
            for (int i = 0; i < 10000; i++) {
                list.add(String.valueOf(i));
            }
            return list;
        }
    };
    // Returns the current thread's unique ID, assigning it if necessary
    public List<Object> get() {
        return threadId.get();
    }
    // remove currentid
    public void remove() {
        threadId.remove();
    }
}

测试代码如下:

public static void main(String[] args)
            throws InterruptedException {

        //  为了复现key被回收的场景,我们使用临时变量
        ThreadLocalMemory memeory = new ThreadLocalMemory();
    
        // 调用
        incrementSameThreadId(memeory);
    
        System.out.println("GC前:key:" + memeory.threadId);
        System.out.println("GC前:value-size:" + refelectThreadLocals(Thread.currentThread()));
    
        // 设置为null,调用gc并不一定触发垃圾回收,但是可以通过java提供的一些工具进行手工触发gc回收。
        memeory.threadId = null;
        System.gc();
    
        System.out.println("GC后:key:" + memeory.threadId);
        System.out.println("GC后:value-size:" + refelectThreadLocals(Thread.currentThread()));
    
        // 模拟线程一直运行
        while (true) {
        }
    }

此时我们如何知道内存中存在memory leak呢?
我们可以借助jdk提供的一些命令dump当前堆内存,命令如下:
jmap -dump:live,format=b,file=heap.bin <pid>

然后我们借助MAT可视化分析工具,来查看对内存,分析对象实例的存活状态:

首先打开我们工具提示我们的内存泄漏分析:

这里我们可以确定的是ThreadLocalMap实例的Entry.value是没有被回收的。
最后我们要确定Entry.key是否还在?打开Dominator Tree,搜索我们的ThreadLocalMemory,发现并没有存活的实例。

以上我们复现了ThreadLocal不正当使用,引起的内存泄漏。

所以我们总结了使用ThreadLocal时会发生内存泄漏的前提条件:
①ThreadLocal引用被设置为null,且后面没有set,get,remove操作。
②线程一直运行,不停止。(线程池)
③触发了垃圾回收。(Minor GC或Full GC)

我们看到ThreadLocal出现内存泄漏条件还是很苛刻的,所以我们只要破坏其中一个条件就可以避免内存泄漏,单但为了更好的避免这种情况的发生我们使用ThreadLocal时遵守以下两个小原则:
①ThreadLocal申明为private static final。
Private与final 尽可能不让他人修改变更引用,
Static 表示为类属性,只有在程序结束才会被回收。
②ThreadLocal使用后务必调用remove方法。
最简单有效的方法是使用后将其移除。

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

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

相关文章

031:vue子组件向父组件传递多个参数,父组件2种解析方法

第031个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

6个超好用的视频素材网站,4K/8K高质量,免费下载。

很多视频剪辑和做自媒体的朋友都不知道去哪里找视频素材&#xff0c;而且很多网站的素材可以免费下载但是不能商用&#xff0c;还有需要付费购买使用。下面推荐几个良心网站&#xff0c;视频素材免费下载&#xff0c;还能商用&#xff0c;赶紧收藏起来吧。 1、菜鸟图库 https:…

基于Python和mysql开发的在线音乐网站系统(源码+数据库+程序配置说明书+程序使用说明书)

一、项目简介 本项目是一套基于Python和mysql开发的在线音乐网站系统(&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Python学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经…

qt 正则表达式

以上是正则表达式的格式说明 以下是自己写的正则表达式 22-25行 是一种设置正则表达式的方式&#xff0c; 29-34行 : 29行 new一个正则表达式的过滤器对象 30行 正则表达式 的过滤格式 这个格式是0-321的任意数字都可以输入 31行 将过滤格式保存到过滤器对象里面 32行 将验…

VSCode搭建Django开发环境

文章目录 一、Django二、搭建步骤1. 安装python和VSCode&#xff0c;安装插件2. VSCode打开项目文件夹3. 终端中键入命令&#xff1a;建立虚拟环境4. 选择Python的解释器路径为虚拟环境5. 在虚拟环境中安装Django6.创建Django项目7. 创建app应用8. 运行应用9. 修改配置中文显示…

JVM 虚拟机 ----> Java 类加载机制

文章目录 JVM 虚拟机 ----> Java 类加载机制一、概述二、类的生命周期1、类加载过程&#xff08;Loading&#xff09;&#xff08;1&#xff09;加载&#xff08;2&#xff09;验证&#xff08;3&#xff09;准备&#xff08;4&#xff09;解析&#xff08;5&#xff09;初始…

纯小白安卓刷机1

文章目录 常见的英文意思刷机是什么&#xff1f;为什么要刷机&#xff1f;什么是BL锁&#xff08;BootLoader锁&#xff09;&#xff1f;我的机能够刷机吗&#xff1f;什么是Boot镜像/分区&#xff1f;什么是Recovery镜像/分区&#xff08;缩写为rec&#xff09;&#xff1f;什…

2023-2024年最值得选的Java毕业设计选题大全:2000个热门选题推荐

一、前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; 毕业设计选题非常重要&a…

【数仓建设系列之五】数仓选型架构概览

【数仓建设系列之五】实时数仓选型架构概览 离线数仓&#xff08;Offline Data Warehouse&#xff09;和实时数仓&#xff08;Real-time Data Warehouse&#xff09;是数仓领域两种常见的数据存储和处理架构&#xff0c;它们在数据处理的方式、目标和时间性上有所不同&#xff…

web端三维重建算法-colmap++

vismap vismap 是colmap 版本 &#xff08;1&#xff09; 支持superpoint superglue &#xff08;2&#xff09; 支持netvlad 图像检索 &#xff08;3&#xff09;支持特征点尺度定权 &#xff08;4&#xff09;支持二维码定位 &#xff08;5&#xff09;支持融合gps &#x…

穿破行业增长迷雾,云鲸J4为何能成为“破题之钥”?

文 | 螳螂观察 作者 | 青月 这几年&#xff0c;消费者对于产品的需求一直在变。 像汽车&#xff0c;过去的消费者可能更看重车的安全性、油耗低等&#xff0c;可如今再看消费者对车的需求&#xff0c;车联网服务、自动辅助驾驶等过去被视为“边缘”的能力&#xff0c;正在变…

虚拟机Ubuntu操作系统最基本终端命令(安装包+详细解释+详细演示)

虚拟机及乌班图&#xff08;Ubuntu操作系统&#xff09; 提示&#xff1a;大家需要软件的可以直接在此链接中提取 链接&#xff1a;https://pan.baidu.com/s/1_4VHGTlXjIuVhBINeOuBCA 提取码&#xff1a;nd0c 文章目录 虚拟机及乌班图&#xff08;Ubuntu操作系统&#xff09;终…

医院不良事件管理系统源码 鱼骨图分析 跌倒事件、压疮事件、坠床事件等系统检测,智能上报

医疗不良事件报告系统源码 医疗不良事件报告系统源码旨在建立全面的、统一的医疗不良事件标准分类系统和患者安全术语&#xff0c;使不良事件上报管理更加标准化和科学化。通过借鉴国内外医疗不良事件报告系统的先进经验&#xff0c;根据医疗不良事件的事件类型、处理事件的不…

Linux高性能服务器编程 学习笔记 第二章 IP协议详解

本章从两方面探讨IP协议&#xff1a; 1.IP头部信息。IP头部出现在每个IP数据报中&#xff0c;用于指定IP通信的源端IP地址、目的端IP地址&#xff0c;指导IP分片和重组&#xff0c;指定部分通信行为。 2.IP数据报的路由和转发。IP数据报的路由和转发发生在除目标机器外的所有主…

【SSH】如何删掉远程服务器中的虚拟环境?如何删掉远程服务器中的用户?如何删掉某个文件夹?

文章目录 一、如何删掉远程服务器中的虚拟环境&#xff1f;二、如何删掉远程服务器中的用户&#xff1f;三、如何删掉某个文件夹&#xff1f; 一、如何删掉远程服务器中的虚拟环境&#xff1f; 在Linux系统下删除conda虚拟环境&#xff1a; # 删除虚拟环境 conda remove -n y…

Nginx 中 location 和 proxy_pass 的斜杠问题

location 的斜杠问题比较好理解&#xff0c;不带斜杠的是模糊匹配。例如&#xff1a; location /doc 可以匹配 /doc/index.html&#xff0c;也可以匹配 /docs/index.html。 location /doc/ 强烈建议使用这种 只能匹配 /doc/index.html&#xff0c;不能匹配 /docs/index…

基于SSM的班主任助理系统的设计与实现

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

Linux 编译安装中的 configure 命令

用了这么久的 Linux 系统&#xff0c;也许你会发现&#xff0c;在编译安装中&#xff0c;有的服务编译安装需要执行 configure 命令&#xff0c;而有的却不需要&#xff0c;这是为什么呢&#xff1f;也是不是像我一样一头雾水呢&#xff1f;其实这取决于服务的构建系统和配置方…

k8skubectl陈述式及声明式资源管理

k8s:kubectl陈述式及声明式资源管理 一、陈述式资源管理方法1.陈述式资源管理概念2.基本信息查看&#xff08;1&#xff09;查看版本信息&#xff08;2&#xff09;查看资源对象简写&#xff08;3&#xff09;查看集群信息&#xff08;4&#xff09;配置kubectl自动补全&#x…

搭建个人博客系统

效果图&#xff1a; 博客网址&#xff1a; 行秋http://8.137.35.5:8093/#/Home源码链接&#xff1a; QiuShicheng/Qiu-blog (github.com)https://github.com/QiuShicheng/Qiu-blog 视频参考&#xff1a; B站最通俗易懂手把手SpringBootVue项目实战-前后端分离博客项目-Java…