当前位置: 首页 > article >正文

【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD

🔥 2025本人正在沉淀中… 博客更新速度++

👍 欢迎点赞、收藏、关注,跟上我的更新节奏

📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》

前言

经过了上一篇的学习,聪明的你一定知道了ThradLocal的怎么样使用的。

【Java并发】【ThreadLocal】适合初学体质的ThreadLocal

下面,跟上主播的节奏,马上开始ThreadLocal源码的阅读( ̄▽ ̄)"

内部结构

如下图所示,我们可以知道,每个线程,都有自己的threadLocals字段,指向ThreadLocalMap

ThreadLocalMap中有一个Entry数组(table),用来存储我们set进ThreadLocal的值。

Entry的key指向ThreadLocal(弱引用),value就是我们set的值(强引用)。

在这里插入图片描述

Set流程

在这里插入图片描述

// set方法入口
public void set(T value) {Thread t = Thread.currentThread();// 获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 设置值map.set(this, value);} else {// 为当前线程创建ThradLocalMapcreateMap(t, value);}
}// getMap方法。获取当前线程,ThreadLocalMap
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

map为null的情况,开始初始化

初始化

void createMap(Thread t, T firstValue) {// 为当前线程设置ThreadLocalMap// key是TheadLocal,value是我们要塞入ThreadLocal线程的值t.threadLocals = new ThreadLocalMap(this, firstValue);
}

具体new的逻辑

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 设置table大小,初始容量为16。ThreadLocalMap的table就是用来存Entry的。table = new Entry[INITIAL_CAPACITY];	// 哈希算法算法,决定新的Entry插入到哪个槽里,后面会具体说这个。int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);// 当前在ThreadLocalMap中的Entry数量size = 1;// 设置扩容的阈值,2/3的时候扩容setThreshold(INITIAL_CAPACITY);
}

初始容量,16个,强制要求为2的幂次,用于优化位运算性能。通过静态final修饰确保全局唯一且不可修改。

/*** The initial capacity -- MUST be a power of two.*/
private static final int INITIAL_CAPACITY = 16;

扩容阈值,2/3 的时候扩容

/*** Set the resize threshold to maintain at worst a 2/3 load factor.*/
private void setThreshold(int len) {threshold = len * 2 / 3;
}

好了,让我们具体来说说说是怎么哈希的

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);

为什么是 &? firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)?

当容量 INITIAL_CAPACITY 是 2 的幂(如 16)时,INITIAL_CAPACITY - 1 的二进制形式为全 1(例如 15 -> 1111)。此时,hash & (INITIAL_CAPACITY - 1) 等效于 hash % INITIAL_CAPACITY,但位运算的效率远高于取模运算。

firstKey.threadLocalHashCode,这个咋来的?

private final int threadLocalHashCode = nextHashCode();// nextHashCode方法
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}// nextHashCode,从0开始的hashCode
private static AtomicInteger nextHashCode = new AtomicInteger();// HASH_INCREMENT
private static final int HASH_INCREMENT = 0x61c88647;

为什么这个魔数是0x61c88647?

0x61c88647 近似于 (√5 - 1)/2 * 2^32(黄金分割比例的 32 位扩展)。这种设计确保哈希码在 2 的幂容量下均匀分布,减少冲突概率。

map不为null的情况,直接set

private void set(ThreadLocal<?> key, Object value) {// ThreadLocalMap的table,table是个数组里面是放Entry的Entry[] tab = table;int len = tab.length// hash到哪个槽位(上面初始化的时候,具体有说)int i = key.threadLocalHashCode & (len-1);// 判断当前的槽是否已经有Entry了// - 如果有:就for循环table,看是不是这次要更新的槽。//      - 是的话更新。// 		- 不是的话,说明存在哈希冲突,需要通过开放地址法,找到为空的槽插入。// - 如果没有:就不执行下面的for循环,直接插入即可。// ===== ThreadLocal哈希冲突解决 =====// 我们都知道,这里的哈希冲突解决方案是 ---》 开放地址法(线性探测法)// 这里的处理:如果enty不为null的话,就一直i+1往下找下一个,直到table[i] == null。//(下面有nextIndex的代码,就是i+1。)for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 如果当前Entry的key,就是这次set需要的Entry的key,这里就更新if (k == key) {e.value = value;return;}// 如果这个Entry的key为null(可能是线程执行完被GC了)if (k == null) {// 清除这个Entry(后面会具体说怎么清除的)replaceStaleEntry(key, value, i);return;}}// 对该槽赋值tab[i] = new Entry(key, value);// table中Entry数量+1int sz = ++size;// 1、cleanSomeSlots清理没用的槽// 2、如果清空失败 并且 table中的Entry数量大于等于扩容阈值if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();	// 重新hash
}

寻找下一个地址的方法nextIndex。

private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}

rehash做了什么?

private void rehash() {// 清除过期的Entry(Entry的key为null就是过期的意思,后面会说具体说怎么清理的)expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresis// 通过主动提前扩容,防止哈希表在临界负载时因性能骤降影响用户体验// 当前Entry的数量 大于等于 扩容阈值的1/2if (size >= threshold - threshold / 4)resize();
}

为什么是threshold - threshold / 4

因为要保持低附载。所以我们也可以说这是1 / 2扩容。

在开放寻址法中,通过主动降低扩容阈值(从 2/3 容量降至 3/4 * 2/3 = 1/2 容量),确保哈希表始终处于低负载状态,从而:减少线性探测的冲突概率,平滑性能波动,避免滞后现象,以可控的内存增长换取稳定的高性能。

那resize方法做了什么捏?省流版:扩容2倍扩容,旧的table里的Entry,重新哈希放入新的table里。

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 2倍当前长度大小int newLen = oldLen * 2;Entry[] newTab = new Entry[newLen];int count = 0;// 遍历旧table里的Entry,将Entry重新哈希,放入到新的table里for (int j = 0; j < oldLen; ++j) {Entry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();// 清理失效的Entryif (k == null) {e.value = null; } else {// 哈希到新的槽位int h = k.threadLocalHashCode & (newLen - 1);// 开放地址法解决哈希冲突,如果这个节点有值了,就i+1找下一个槽位while (newTab[h] != null)h = nextIndex(h, newLen);	// h+1,寻找下一个槽位// 新table里,节点的赋值newTab[h] = e;// 旧table的有效Entry数量 + 1count++;}}}// 设置新的扩容阈值(数组长度的 2/3)setThreshold(newLen);// 修改为新的table的sizesize = count;// 指向新的tabletable = newTab;
}// 设置新的扩容阈值
private void setThreshold(int len) {threshold = len * 2 / 3;
}

Get流程

在这里插入图片描述

get方法入口

public T get() {Thread t = Thread.currentThread();// 获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// 根据当前线程的ThreadLocal获取EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// 当前线程没有ThreadLocalMapreturn setInitialValue();
}

获取Entry,getEntry方法。

private Entry getEntry(ThreadLocal<?> key) {// 获取槽位int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];// 如果这个槽不为null 且 这个槽位Entry的key是当前的ThreadLocalif (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);	// 当前槽找不到这次要获取的key
}

如果找不到,执行getEntryAfterMiss方法。

为什么会找不到?

可能是真没有,也有可能是哈希冲突导致的槽位一直++,所以Entry被放入到了后面。

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;// 清除过期的Entryif (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);	// 获取下一个槽位// e变为下一个Entrye = tab[i];}// 实在找不到return null;
}

我们来看看,如果当前线程没有ThreadLocalMap,会怎么处理setInitialValue方法

private T setInitialValue() {// 初始value值,默认为null。子类可以重写,返回不同的初始值。T value = initialValue();Thread t = Thread.currentThread();// 获取当前线程的ThreadLocal。(上面有说过这个方法,就是t.threadLocals。)ThreadLocalMap map = getMap(t);// 这个和的set流程一样了。有就set初始值进去,没有就创建map。if (map != null) {map.set(this, value);} else {createMap(t, value);}// 返回初始值return value;
}

initialValue,默认返回null,子类可以重写。

protected T initialValue() {return null;
}

下面,主播为大家表演一手,子类重写。

// 子类重写
public class MyThreadLocal extends ThreadLocal {@Overrideprotected Object initialValue() {return "aaaa";}
}// 测试类 
public class MyThreadLocalTest {public static void main(String[] args) {MyThreadLocal myThreadLocalTest = new MyThreadLocal();String o = (String) myThreadLocalTest.get();System.out.println(o);	// 输出aaaa}
}

删除Entry

在ThreadLocal中存在2种删除方法。

探测删除(线性探测清理)

  • 对应方法expungeStaleEntry(int staleSlot)

  • 核心目标:清理当前过期的 Entry,并重新整理哈希表,解决因哈希冲突导致的 Entry 位置偏移问题。

  • 省流版流程:做了2件事情。

    1. 清空当前Entry 。
    2. 判断后续Entry是否需要清除,需要就清除,不需要的话,就重新哈希放到table里。
private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 清理当前槽点的数据tab[staleSlot].value = null;tab[staleSlot] = null;size--;Entry e;int i;// 从当前槽点开始向后遍历,判断每个Entry是否需要清空for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 失效的Entry,就给它清除if (k == null) {e.value = null;tab[i] = null;size--;} else {// ====没有失效的Entry====// 重新哈希槽位,Entry重新放入table中int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// 哈希冲突,开放址法while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}// 返回停止的槽位return i;
}

⚠️ 探测性修复是有局限性的,并不会从当前要删除的槽点一直往后遍历到table的所有槽点,它也会在某个为null的槽点中停下来。(🤔不过话又说回来了,真扫了整个table,性能包炸的)

for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;		// 当tab[i]为null会停止清除,即使tab[i+1]是过期的Entryi = nextIndex(i, len))

启发式清除(概率性扫描)

对应方法cleanSomeSlots(int i, int n)
核心目标:以较低的成本扫描部分槽位,清理可能的过期 Entry,避免全表扫描的性能开销。

// 入参解释:
// - i,一个为null的槽位。
// - n,一般传的是table的长度。
private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;// 一直修改len的值,如果len的默认值为16,16 -> 8 -> 4 -> 2 -> 0do {i = nextIndex(i, len);Entry e = tab[i];// 当前节点是过期的if (e != null && e.get() == null) {n = len;removed = true;// 调用探测删除方法i = expungeStaleEntry(i);}} while ( (n >>>= 1) != 0);return removed;
}

while ((n >>>= 1) != 0) 是一种高效且巧妙的设计:

  1. 位运算控制循环次数:以 O(log n) 时间实现启发式扫描。
  2. 平衡性能与清理效果:避免全表扫描,但覆盖足够多的槽位。
  3. 自适应哈希表容量:天然适配 2 的幂次容量的哈希表。

Remove流程

上面介绍了Entry是怎么删除的,这里来看看,remove到底干了什么勒?😄

 public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());	// 获取当前线程的ThreadLocalMapif (m != null)m.remove(this);}

具体的remove方法,我们总结下做了啥?

  1. 哈希开始的槽位,找到要删除的Entry
  2. 断开Entry对ThreadLocal key的弱引用(置为null)
  3. 探测式删除Entry。
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 获取开始的槽位int i = key.threadLocalHashCode & (len-1);// 可能出现哈希冲突,所以要一直向后找Entry。for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();	// 删除key的弱引用expungeStaleEntry(i);	// 上面介绍过勒,这个是探测式删除。return;}}
}

为什么会内存泄漏?

这就不得不提到我们的Entry了,我们可以看到,我们的Key是一个弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);  // 调用父类 WeakReference 的构造函数,将 k 作为弱引用存储value = v;}
}

那么,弱引用会怎么样勒?来主播带大家回忆下,Java的四种引用:

强引用 (Strong Reference)

  • 清除时机:当对象没有任何强引用指向时,会在下一次GC时被回收。即使内存不足,也不会被回收(可能导致OOM)。
 Object obj = new Object();  // 强引用

软引用 (SoftReference)

  • 清除时机:当内存不足时(即将抛出OutOfMemoryError前),JVM会尝试回收。
SoftReference<Object> softRef = new SoftReference<>(new Object());

弱引用 (WeakReference)

  • 清除时机:只要发生垃圾回收,无论内存是否充足,对象都会被回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());

虚引用 (PhantomReference)

  • 清除时机: 对象被回收后,虚引用会被放入关联的ReferenceQueue,需手动处理。无法通过虚引用获取对象实例,仅用于追踪对象回收状态。
// 1. 必须绑定引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object heavyObject = new Object(); // 假设这是一个占用大量资源的对象// 2. 创建虚引用(必须关联队列)
PhantomReference<Object> phantomRef = new PhantomReference<>(heavyObject, queue);// 3. 关键特性:无法通过虚引用获取对象
System.out.println(phantomRef.get()); // 输出 null ❌// 4. 手动解除强引用(触发回收条件)
heavyObject = null;// 5. 强制触发垃圾回收
System.gc();// 6. 检查队列:当对象被回收后,虚引用会被加入队列
Reference<?> ref = queue.remove(500); // 阻塞500ms等待队列
if (ref == phantomRef) {System.out.println("对象已被回收,可执行后续清理操作 ✅");// 例如:若对象管理了堆外内存,可在此释放
}

ok,前面铺垫了这么久,让我们分析下,内存泄漏的原因:

Key 的弱引用特性

  • ThreadLocal 实例失去强引用时(例如 threadLocal = null),由于 Entry 的 key 是弱引用,GC 会回收该 ThreadLocal 实例,此时 Entry 的 key 变为 null
  • 问题Entryvalue 仍是强引用,无法被自动回收。

在这里插入图片描述

线程长期存活

如果线程是线程池的核心线程(长期存活),它的 ThreadLocalMap 会一直存在。即使 key 被回收,value 仍然通过 Entry 的强引用保留在内存中。

代码模拟,没有及时remove,导致OOM:

public class ThreadLocalOOMExample {// 设置 JVM 参数:-Xmx64m -Xms64m (限制堆内存为 64MB)public static void main(String[] args) throws InterruptedException {ExecutorService executor = Executors.newFixedThreadPool(5); // 线程池复用线程for (int i = 0; i < 1000; i++) { // 提交 1000 个任务executor.submit(() -> {ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();try {// 每个任务创建一个 1MB 的对象,存到 ThreadLocal 中threadLocal.set(new BigObject());// 模拟业务逻辑(不调用 remove())} finally {// 此处故意不调用 threadLocal.remove()}});Thread.sleep(10); // 控制任务提交速度}executor.shutdown();}static class BigObject {// 每个对象占用 1MB 内存private final byte[] data = new byte[1024 * 1024];}
}

运行结果

Exception in thread "pool-1-thread-2" java.lang.OutOfMemoryError: Java heap spaceat ThreadLocalOOMExample$BigObject.<init>(ThreadLocalOOMExample.java:23)at ThreadLocalOOMExample.lambda$main$0(ThreadLocalOOMExample.java:15)...

后话

聪明的你,是不是对ThreadLocal有更多的了解呢?

  • ThreadLocalMap的数据结构是怎么样
  • value是怎么set进去
  • 怎么get到我们set的value
  • 如何删除过期的Entry
  • 内存泄漏的可能原因

相信聪明的你一定有了答案(‾◡◝)

相关文章:

【源码】【Java并发】【ThreadLocal】适合中学者体质的ThreadLocal源码阅读

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f4da;欢迎订阅专栏…...

背包问题模板

文章目录 01背包题意思路代码优化 完全背包题意思路代码优化 多重背包题意思路代码优化 分组背包题意思路代码 01背包 特点&#xff1a;每件物品最多只能用一次 01背包问题 题意 给出每件物品的体积v,价值w,求解能装入背包的的物品的最大价值&#xff0c;并且每件物品只能选一…...

Redis 处理读请求

在前文“Redis 接收连接”中&#xff0c;Redis 将接收的客户端连接加入了 epoll 中监听&#xff0c;同时还设置了读事件处理器 connSocketEventHandler。 假设现在客户端向 Redis 发来一条 set key value 命令。 事件循环 aeProcessEvents 在事件循环 aeProcessEvents 中会调…...

Sentinel源码—8.限流算法和设计模式总结二

大纲 1.关于限流的概述 2.高并发下的四大限流算法原理及实现 3.Sentinel使用的设计模式总结 3.Sentinel使用的设计模式总结 (1)责任链模式 (2)监听器模式 (3)适配器模式 (4)模版方法模式 (5)策略模式 (6)观察者模式 (1)责任链模式 一.责任链接口ProcessorSlot 二.责…...

VulnHub-DarkHole_1靶机渗透教程

VulnHub-DarkHole_1靶机渗透教程 1.靶机部署 [Onepanda] Mik1ysomething 靶机下载&#xff1a;https://download.vulnhub.com/darkhole/DarkHole.zip 直接使用VMware打开就行 导入成功&#xff0c;打开虚拟机&#xff0c;到此虚拟机部署完成&#xff01; 注意&#xff1a…...

Keil MDK‑5 中使用 GNU ARM GCC 的 -Wno-* 选项屏蔽编译警告

在项目编译过程中&#xff0c;我们常常会遇到许多警告提示&#xff1b;而在有些情况下&#xff0c;当我们已经了解这些警告的原因时&#xff0c;可以选择忽略它们&#xff0c;从而减少干扰&#xff0c;集中精力修复其他更重要的问题。 一、添加屏蔽警告的编译选项 &#xff08…...

边缘计算全透视:架构、应用与未来图景

边缘计算全透视&#xff1a;架构、应用与未来图景 一、产生背景二、本质三、特点&#xff08;一&#xff09;位置靠近数据源&#xff08;二&#xff09;分布式架构&#xff08;三&#xff09;实时性要求高 四、关键技术&#xff08;一&#xff09;硬件技术&#xff08;二&#…...

爬虫学习——下载文件和图片、模拟登录方式进行信息获取

一、下载文件和图片 Scrapy中有两个类用于专门下载文件和图片&#xff0c;FilesPipeline和ImagesPipeline&#xff0c;其本质就是一个专门的下载器&#xff0c;其使用的方式就是将文件或图片的url传给它(eg:item[“file_urls”])。使用之前需要在settings.py文件中对其进行声明…...

路由器转发规则设置方法步骤,内网服务器端口怎么让异地连接访问的实现

在路由器上设置端口转发&#xff08;Port Forwarding&#xff09;可以将外部网络流量引导到特定的局域网设备&#xff0c;这对于需要远程访问服务器、摄像头、游戏主机等设备非常有用。 登录路由器管理界面&#xff0c;添加端口转发规则让外网访问内网的实现教程分享。以下是设…...

MQ底层原理

RabbitMQ 概述 RabbitMQ 是⼀个开源的⾼性能、可扩展、消息中间件&#xff08;Message Broker&#xff09;&#xff0c;实现了 Advanced Message Queuing Protocol&#xff08;AMQP&#xff09;协议&#xff0c;可以帮助不同应⽤程序之间进⾏通信和数据交换。RabbitMQ 是由 E…...

本地部署DeepSeek-R1模型接入PyCharm

以下是DeepSeek-R1本地部署及接入PyCharm的详细步骤指南,整合了视频内容及官方文档核心要点: 一、本地部署DeepSeek-R1模型 1. 安装Ollama框架 ​下载安装包 访问Ollama官网(https://ollama.com/download)Windows用户选择.exe文件,macOS用户选择.dmg包。 ​安装验证 双击…...

jvm-描述符与特征签名的区别

在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;存储的是方法签名&#xff0c;而不是仅仅方法描述符。方法签名包含了方法的参数类型和返回值类型的信息&#xff0c;而方法描述符通常指的是仅包含参数类型的那部分信息。为了更清晰地理解这两者的区别以及它们如何在JVM…...

Java基于SpringBoot的企业车辆管理系统,附源码+文档说明

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…...

QT调用ffmpeg库实现视频录制

可以通过QProcess调用ffmpeg命令行,也可以直接调用ffmpeg库,方便。 调用库 安装ffmpeg ffmpeg -version 没装就装 sudo apt-get update sudo apt-get install ffmpeg sudo apt-get install ffmpeg libavdevice-dev .pro引入库路径,引入库 LIBS += -L/usr/lib/aarch64-l…...

百度 Al 智能体心响 App 上线

百度 AI 智能体心响 App 于 2025 年 4 月 17 日低调上线安卓应用市场&#xff0c;4 月 22 日正式登陆各大安卓应用市场。以下是对这款应用的详细介绍&#xff1a; 产品定位&#xff1a;这是一款以 “AI 任务完成引擎” 为核心的手机端超级智能体产品&#xff0c;定位为 “复杂任…...

进阶篇 第 2 篇:自相关性深度解析 - ACF 与 PACF 图完全指南

进阶篇 第 2 篇&#xff1a;自相关性深度解析 - ACF 与 PACF 图完全指南 (图片来源: Negative Space on Pexels) 欢迎来到进阶系列的第二篇&#xff01;在上一篇&#xff0c;我们探讨了更高级的时间序列分解技术和强大的指数平滑 (ETS) 预测模型。ETS 模型通过巧妙的加权平均捕…...

【Java面试笔记:基础】7.int和Integer有什么区别?

在Java中&#xff0c;int和Integer虽然都用于表示整数值&#xff0c;但它们在本质、用法和特性上有显著差异。 1. int 和 Integer 的区别 int&#xff1a; 原始数据类型&#xff1a;int 是 Java 的 8 个原始数据类型之一&#xff0c;用于表示整数。性能优势&#xff1a;直接存…...

鸿蒙移动应用开发--渲染控制实验

任务&#xff1a;使用“对象数组”、“ForEach渲染”、“Badge角标组件”、“Grid布局”等相关知识&#xff0c;实现生效抽奖卡案例。如图1所示&#xff1a; 图1 生肖抽奖卡实例图 图1(a)中有6张生肖卡可以抽奖&#xff0c;每抽中一张&#xff0c;会通过弹层显示出来&#xf…...

AI代表企业签订的合同是否具有法律效力?

AI代表企业签订的合同是否具有法律效力&#xff1f; 首席数据官高鹏律师团队编著 在数字经济高速发展的今天&#xff0c;人工智能&#xff08;AI&#xff09;技术已广泛应用于商业活动&#xff0c;包括合同起草、审查甚至签署环节。然而&#xff0c;AI代表企业签订的合同是否具…...

安宝特分享|AR智能装备赋能企业效率跃升

AR装备开启智能培训新时代 在智能制造与数字化转型浪潮下&#xff0c;传统培训体系正面临深度重构。安宝特基于工业级AR智能终端打造的培训系统&#xff0c;可助力企业构建智慧培训新生态。 AR技术在不同领域的助力 01远程指导方面 相较于传统视频教学的单向输出模式&#x…...

SpringCloud组件—Eureka

一.背景 1.问题提出 我们在一个父项目下写了两个子项目&#xff0c;需要两个子项目之间相互调用。我们可以发送HTTP请求来获取我们想要的资源&#xff0c;具体实现的方法有很多&#xff0c;可以用HttpURLConnection、HttpClient、Okhttp、 RestTemplate等。 举个例子&#x…...

模型 螃蟹效应

系列文章分享模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。个体互钳&#xff0c;团队难行。 1 螃蟹效应的应用 1.1 教育行业—优秀教师遭集体举报 行业背景&#xff1a;某市重点中学推行绩效改革&#xff0c;将班级升学率与教师奖金直接挂钩&#xff0c;打破原…...

符号速率估计——小波变换法

[TOC]符号速率估计——小波变换法 一、原理 1.Haar小波变换 小波变换在信号处理领域被成为数学显微镜&#xff0c;不同于傅里叶变换&#xff0c;小波变换可以观测信号随时间变换的频谱特征&#xff0c;因此&#xff0c;常用于时频分析。   当小波变换前后位置处于同一个码元…...

【架构】ANSI/IEEE 1471-2000标准深度解析:软件密集型系统架构描述推荐实践

引言 在软件工程领域&#xff0c;架构设计是确保系统成功的关键因素之一。随着软件系统日益复杂化&#xff0c;如何有效描述和沟通系统架构成为了一个亟待解决的问题。ANSI/IEEE 1471-2000&#xff08;正式名称为"推荐软件密集型系统架构描述实践"&#xff09;应运而…...

python兴趣匹配算法

python兴趣匹配算法&#xff0c;用于推荐好友&#xff0c;短视频推荐等等领域 功能列表&#xff1a; 1.用户类的定义&#xff0c;存储用户的基本信息和兴趣。 2.计算两个用户之间兴趣的匹配分数&#xff0c;比较每一位是否相同。 3.根据匹配分数对候选人进行排序。 4.提供交互…...

每日算法-250422

每日算法 - 250422 1561. 你可以获得的最大硬币数目 题目 思路 贪心 解题过程 根据题意&#xff0c;我们想要获得最大的硬币数目。每次选择时&#xff0c;有三堆硬币&#xff1a;最大的一堆会被 Alice 拿走&#xff0c;最小的一堆会被 Bob 拿走&#xff0c;剩下的一堆&#xf…...

【MATLAB第116期】基于MATLAB的NBRO-XGBoost的SHAP可解释回归模型(敏感性分析方法)

【MATLAB第116期】基于MATLAB的NBRO-XGBoost的SHAP可解释回归模型&#xff08;敏感性分析方法&#xff09; 引言 该文章实现了一个可解释的回归模型&#xff0c;使用NBRO-XGBoost&#xff08;方法可以替换&#xff0c;但是需要有一定的编程基础&#xff09;来预测特征输出。该…...

微信公众号消息模板推送没有“详情“按钮?无法点击跳转

踩坑&#xff01;&#xff01;&#xff01;&#xff01;踩坑&#xff01;&#xff01;&#xff01;&#xff01;踩坑&#xff01;&#xff01;&#xff01;&#xff01; 如下 简单说下我的情况&#xff0c;按官方文档传参url了 、但就是看不到查看详情按钮 。如下 真凶&#x…...

WHAT - 静态资源缓存穿透

文章目录 1. 动态哈希命名的基本思路2. 具体实现2.1 Vite/Webpack 配置动态哈希2.2 HTML 文件中动态引用手动引用使用 index.html 模板动态插入 2.3 结合 Cache-Control 避免缓存穿透2.4 适用于多环境的动态策略 总结 在多环境部署中&#xff0c;静态资源缓存穿透是一个常见问题…...

电动单座V型调节阀的“隐形守护者”——阀杆节流套如何解决高流速冲刷难题

电动单座V型调节阀的“隐形守护者”——阀杆节流套如何解决高流速冲刷难题&#xff1f; 在工业自动化控制中&#xff0c;电动单座V型调节阀因其精准的流量调节能力&#xff0c;成为石油、化工等领域的核心设备。然而&#xff0c;长期高流速工况下&#xff0c;阀芯与阀座的冲刷腐…...