Java并发——ThreadLocal总结

news2024/12/23 3:48:24

概述

并发问题,有时候,可以用ThreadLocal方式来避免。

ThreadLocal,顾名思义,就是线程自己的,独享的,就像线程栈是线程独享的一样。

本文讨论三点:

  1. 基本用法
  2. 设计原理
  3. 父子线程

基础用法

考虑类A有doSync方法,可能会被并发调用. 因为SimpleDateFormat非线程安全,所以在方法内new创建。

public void doSync(){
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    simpleDateFormat.format(new Date());
    // .... some complex ops
}
复制代码

可以优化为以下方案,让每个线程都有自己的SimpleDateFormat, 从而不用每次调用都new一个:

private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
    SimpleDateFormat simpleDateFormat = sdf.get();
    simpleDateFormat.format(new Date());
    // .... some complex ops
}
复制代码

说明:本段代码可能在某个类A中, 方法doSync可能会被并发调用。

如果在doSync内部new一个SimpleDateFormat,同一个线程调用也要每次都new一个,有损性能,其实同一个线程可以共享一个。所以,可以用一个ThreadLocal类型的变量,包含一个SimpleDateFormat。 这里没有调用remove,是希望每个线程里都常驻一个日期格式化对象。

另外的一个栗子是,我们在web开发里,有时候会跨层传播一些上下文信息,会使用ThreadLocal,譬如在某个filter里使用set方法设置,然后结束的时候remove。

ThreadLocal主要方法说明:

  • withInitial : 接受一个Supplier(函数接口,定义了get方法,顾名思义,就是提供者),提供什么?当然是提供要放在ThreadLocal内的变量,因为是要在线程内创建,不是马上要,所以需要的是一个supplier
  • set: 设置当前线程ThreadLocal包含的值
  • get: 获取当前线程ThreadLocal包含的值
  • remove:移除当前线程ThreadLocal包含的值

设计原理

为了更加直观的感受ThreadLocal和ThreadLocal所容纳变量的关系,可以继续看下图。 ThreadLocal仅仅是一个访问者,线程独占的变量在各自线程的ThreadLocalMap中。

不过需要注意的是,图中,ThreadLocal对象T1本身的引用,有对象A,线程1,线程2,线程3一共4个持有者。

我们还是用上文日期格式化的代码来说明对应关系:

 // 类A的代码片段
private ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public void doSync(){
    SimpleDateFormat simpleDateFormat = sdf.get();
    simpleDateFormat.format(new Date());
    // .... some extremely complex ops
	  
}
复制代码
  • 变量X: 就是 ThreadLocal.withInitial 里面 Supplier方法的返回值,一个SimpleDateFormat对象
  • ThreadLocal对象T1: 就是类A里定义的成员变量 ThreadLocal sdf
  • ThreadLocalMap: 一种Map数据结构,类似HashMap,线程框架里自己实现一个Map,应该是不想和集合框架耦合吧

问题1: 为什么要用一个Map呢?

因为这种A对象可能有很多个,变量X,ThreadLocal对象T1都会有很多个。

问题2: 有人说ThreadLocal有内存泄漏,是什么意思?

首先我们明确一下内存泄漏: 不会再使用的对象或者变量,占用着内存,且无法被GC掉,称为内存泄漏。 ThreadLocal在线程的ThreadLocalMap中,Key是ThreadLocal对象, Value是变量X副本,泄漏的可能是Key和Value。

案例中的日期格式化工具,仅仅在A的代码片段里有用,而当A对象GC-Root不可达要被干掉了,ThreadLocal对象T1的强引用sdf就没有了,而线程1,2,3里的各自ThreadLocalMap中还有。当不规范使用的时候,或者就是倔强,不remove。久而久之,就会有很多无用的Key和Value充斥着ThreadLocalMap。

但是呢,倔强的我回想了一下,其实往往都没事,这么久了,我都没删啊,也没遇到泄漏啊!

作为框架设计者自然会考虑到,为了方便这些上帝使用框架(Java程序员),从2点分别针对Key和value的泄漏:

  1. 使用ThreadLocal弱引用作为Key,当ThreadLocal变量只有弱引用时,就会被GC掉,ThreadLocalMap里的key就会指向null(或者说Key就是null)
  2. ThreadLocalMap当rehash的时候,会干掉key为null对应的Value (这或许也是自己实现一个Map的原因吧)

所以,如果没有rehash,泄漏还是存在的,只不过,一般很难达到觉察的程度。

下面,从源码的角度佐证一下针对泄漏所做的2个要点。

弱引用:

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);   // ThreadLocal k 在这里开始被弱引用指向了
        value = v;
    }
}
复制代码

清理key为null的value:

// ThreadLocal.ThreadLocalMap.rehash
private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}


// ThreadLocal.ThreadLocalMap.resize
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

复制代码

父子线程

有时候,执行业务逻辑需要异步,但是当前线程的ThreadLocal变量,怎么传递给子线程呢?

ThreadLocal有个子类InheritableThreadLocal, 基本使用如下:

static class A {
    private InheritableThreadLocal<HashMap<String, String>> map1 = new InheritableThreadLocal<HashMap<String, String>>(){
        @Override
        protected HashMap<String, String> initialValue() {
            return new HashMap<>(8);
        }
    };
    private ThreadLocal<HashMap<String, String>> map2 = new ThreadLocal<HashMap<String, String>>(){
        @Override
        protected HashMap<String, String> initialValue() {
            return new HashMap<>(8);
        }
    };
    public void doAsync(){
        map1.get().put("name", "zhangsan");
        map2.get().put("name", "zhangsan");
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("map1: " + map1.get().get("name"));  // 子线程t可以读取到map1的name
                System.out.println("map2: " + map2.get().get("name"));  // 却无法读取到map2的name
            }
        }, "A-SUB-0");

        t.start();
    }
}

复制代码

怎么传递的呢?

创建线程的时候,Thread类的构造函数会判断当前线程中是否存在InheritableThreadLocal, 如果有,就会拷贝一份。

// Thread类构造函数执行的代码片段: 体现了对inheritableThreadLocal的复制
if (parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
复制代码

仅仅是在创建线程的时候,会发生一次拷贝, 拷贝的是ThreadLocalMap里的Entry数组,即包含Key:ThreadLocal对象和Value对象。

  • 后续父线程内有增减ThreadLocal,都和子线程无关。所以和线程池结合使用的时候,需要特别注意一下。
  • Key和Value都是引用拷贝,所以,同一个ThreadLocal Key和对应的Value变化,父子线程是共享的

伏笔

写到这里,发现还遗漏了一个知识点,就是ThreadLocalMap这个数据结构怎么实现的。

可以带着这几个问题去看下源码,本文暂时先留下伏笔吧~

  • 如何仅仅用一个数组来实现Map
  • 如何解决hash冲突
  • 如何扩容?

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

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

相关文章

JMS规范及落地产品

目录 一、JMS是什么 二、MQ中间件的其他落地产品 三、JMS的组成结构和特点 四、JMS的可靠性 &#xff08;重要&#xff09; &#xff08;一&#xff09;PERSISTENT&#xff1a;持久性 &#xff08;二&#xff09;事务 &#xff08;三&#xff09;Acknowledge&#xff1a…

C#,图像二值化(01)——二值化算法综述与二十三种算法目录

图像二值化&#xff0c;就是把彩色&#xff08;先转为灰色图&#xff09;最终转为黑白两色图片的计算过程。 看似极其简单&#xff0c;但人们研究了几十年&#xff0c;却始终未达到至臻境界的问题。 本文简要介绍了 图像二值化的算法原理、分类及二十三种算法的目录&#xff…

freeswitch的gateway配置方案

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 在voip的网络模型中&#xff0c;网关是我们经常会遇到的概念。 在freeswitch中&#xff0c;如何配置gateway&#xff0c;如何使用好gateway的模型和功能。 本节简单介绍fs中gateway相关的配置方案。 环境 centos&am…

csp-202209

202209题目一&#xff1a;如此编码【100分】题目二&#xff1a;何以包邮&#xff1f;【100分】题目三&#xff1a;防疫大数据【100分】题目一&#xff1a;如此编码【100分】 比较简单的题&#xff0c;根据题意计算一遍就行 一定要关注csp题目中的提示&#xff0c;这个是很有用…

达梦数据IPO过会:拟募资24亿 光谷“扫地僧”冯裕才将敲钟

雷递网 雷建平 12月23日武汉达梦数据库股份有限公司&#xff08;简称&#xff1a;“达梦数据”&#xff09;日前IPO过会&#xff0c;准备在科创板上市。达梦数据计划募资23.51亿元。其中&#xff0c;3.52亿元用于集群数据库管理系统升级项目&#xff0c;3.43亿元用于高性能分布…

[翻译+笔记]变分自编码器:从AutoEncoder到Beta-VAE

与GAN的那篇笔记相同, 做一下笔记. 并不是全文翻译, 只翻译一部分. 原文地址: from AutoEncoder to Beta-VAE 0. 前言 自编码器是用来重构高维数据的&#xff0c;它利用一个有bottleneck层的神经网络。bottleneck层获取压缩的潜在编码&#xff0c;这样将嵌入向量以低维表示可…

Activity生命周期

Activity生命周期 1.Activity状态 1.基本状态 运行&#xff0c;active。位于最前台&#xff0c;可以和用户交互的激活状态。暂停&#xff0c;pause&#xff0c;被透明或者Dialog覆盖&#xff0c;此时可见失去焦点但是不允许交互。停止&#xff0c;stop&#xff0c;被Active覆盖…

spring提前加载,懒加载,bean的作用域和注入注解讲解

前言 sping知识随笔笔记&#xff1b;spring提前加载&#xff0c;懒加载&#xff0c;bean的作用域和注入注解讲解 这里写目录标题前言1 depends-on2 bean的作用域3 lazy-init 懒加载4 Autowrite和Resource的区别和使用1 depends-on depends-on 是提前加载&#xff0c;比如在实…

关于node.js版本切换nvm的命令和安装

首先是安装,第一步,搜索下方链接地址下载Releases coreybutler/nvm-windows GitHub 安装应用下载好后直接安装就可以了,或者下载一个压缩包,在下载安装之前建议先将之前下载的node版本给删除,否则会报错。 上面的操作都结束后,那么,下面就需要通过管理员的权限去查…

外汇天眼:利空美元!2023年美国经济将如履薄冰?各大银行预测整体不乐观!

高盛表示&#xff0c;美国经济可能避免衰退。摩根士丹利预计&#xff0c;美国经济在2023年只是避开了衰退&#xff0c;但着陆并不那么软。瑞士信贷认为&#xff0c;美国明年可以避免经济下滑。摩根大通警告称&#xff0c;明年很有可能出现经济衰退。美国银行预测2023年第一季度…

大学宿舍四位舍友皆为软测,3年后的现状~

笔者最近收到测试员好友小H的分享&#xff0c;临年关&#xff0c;他参加了一场大学舍友毕业3年后的聚会&#xff0c;感慨良多。 从2019年至今&#xff0c;这已经是毕业的第3个年头了。小H的寝室大多来自五湖四海&#xff0c;毕业后&#xff0c;能够相聚的时间也少之又少&#…

Android -- 每日一问:如何设计一个照片上传 app ?

经典回答 把自己放在一个面试官的角度&#xff0c;自己先实现一次这个 App &#xff0c;然后自己总结一下你在这次实现中需要哪些能力、需要注意哪些事项。最后&#xff0c;再回过头来看&#xff0c;如果你是面试官&#xff0c;你希望面试者怎么回答才算是符合你的标准的&…

el-table 列的动态显示与隐藏

目录 业务场景 官方链接 实现效果图 使用框架 代码展示 template代码 ①、为什么要给el-table绑定【:key"reload"】&#xff1f; ②、为什么给每个绑定【key"Math.random()"】呢&#xff1f; ③、为什么列改变之后要添加【reload Math.random();…

【HarmonyOS】调测助手安装失败10内部错误

关于鸿蒙开发通过应用调测助手向watch gt 3 手表安装hap时报错。 问题背景&#xff1a; 鸿蒙开发&#xff0c;使用新建工程的helloworld 没有其他修改&#xff0c;生成hap包。然后通过应用调测助手向watch gt 3 手表安装hap时提示 安装失败:10.内部错误。 Sdk&#xff1a; a…

Shiro之授权

授权 1、角色认证 在controller层创建接口 使用shiro中的注解RequiresRoles指定能访问的角色名称 /*** 登录认证角色*/ RequiresRoles("admin") GetMapping("/userLoginRoles") ResponseBody public String userLoginRoles(){System.out.println("…

54 线程最外层异常的处理

前言 之前在 kafka 消费者客户端的一个 case 中曾经看到了这样的了一个情况 我没有配置 "group.id", 然后 kafka 客户端抛出了 InvalidGroupIdException 然后 输出的日志信息 除了类型, 其他 什么都没有, 主要是 么有堆栈信息 这里 来大致看一下 这个问题, 以及…

WooCommerce Product Feed指南 – Google Shopping和Facebook[2022]

在过去十年中&#xff0c;在线购物一直在增加。全球超过 85% 的人更喜欢网上购物而不是光顾实体店。 许多 WooCommerce 商店都做得非常好&#xff0c;销售额是大约几年前的三倍。 您是否知道您也可以立即轻松地将商店销售额翻三倍&#xff1f; 秘诀是什么&#xff1f; 好吧&…

【网络安全】浅识 SQL 注入

前言 SQL 注入&#xff08;SQL Injection&#xff09;是发生在 Web 程序中数据库层的安全漏洞&#xff0c;是网站存在最多也是最简单的漏洞。主要原因是程序对用户输入数据的合法性没有判断和处理&#xff0c;导致攻击者可以在 Web 应用程序中事先定义好的 SQL 语句中添加额外…

AcrGIS Pro一键出图

简介 日常工作中我们经常遇到批量出图的场景,比如对某个县下的各个乡镇分别按照其行政区范围出图、对某个流域/河流按照一定方向纵横的网格排布顺序出图等等要求,ArcGIS Pro对于上述需求提供了一个良好的解决方案——地图系列! 那么应该如何创建一个地图系列呢?ArcGIS Pro…