news 2026/9/12 14:34:56

源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战

源码级剖析 Java ThreadLocal:线程局部变量的存储结构、哈希探测与内存回收实战

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter

ThreadLocal 是 Java 并发编程中最常用也最容易被误用的工具之一。本文以 JDK 源码为线索,从 Thread 类 中的threadLocals字段出发,完整剖析ThreadLocalset/get/remove核心实现、ThreadLocalMap的哈希存储与扩容机制,以及弱引用Entry与内存泄漏之间的关系。读完你不仅能看懂ThreadLocal的每一行关键代码,还能掌握线程池与 Web 容器场景下线程局部变量的正确清理方式,并顺带理解 Netty 的FastThreadLocal为什么更快。

ThreadLocal 是什么:一句话理解线程局部变量

ThreadLocal类提供了线程局部变量(thread-local variables)的get/set实现。它与普通成员变量最大的不同在于:每个线程都可以通过同一个ThreadLocal对象 get/set 出属于自己的专属值,线程之间互不可见、互不干扰。

ThreadLocal实例通常是类中的私有静态变量,常用于将状态与线程关联,典型场景包括:

  • 用户 ID、事务 ID 等请求维度的上下文传递;
  • 数据库连接、Session 等"一线程一实例"的资源持有;
  • 框架内部(如 Spring 的RequestContextHolder、MyBatis 的 SqlSession 管理)的隐式传参。

tips:在类中定义ThreadLocal变量时,一般在定义时就进行实例化,避免多次创建或空指针问题。

如上图所示:Thread1Thread2各自持有一个独立的ThreadLocalMap,即使它们操作的是同一个threadLocalA对象,存储的值也完全独立(valueA1valueA2)。这正是"同一个 ThreadLocal 对象能为每个线程绑定专属值"的奥秘——set的值实际上被存到了各个调用线程自己的ThreadLocalMap

溯源:Thread 类中的 threadLocals 字段

在阅读ThreadLocal核心 API 之前,必须先看Thread类本身。因为ThreadLocalget/set方法操作的其实都是Thread类中的成员变量。仓库中的 Thread 类源码分析 对这一点做了详细铺垫:

public class Thread implements Runnable { /** 线程名 */ private volatile char name[]; /** 优先级 */ private int priority; /** 是否为守护线程 */ private boolean daemon; /** 线程要执行的目标任务 */ private Runnable target; /** 所属线程组 */ private ThreadGroup group; /** 类加载器 */ private ClassLoader contextClassLoader; /** * ThreadLocal 能为线程设置线程私有变量 就是通过下面这个threadLocals变量完成的, * ThreadLocal的get/set方法就是通过操作 各个线程的 threadLocals 变量实现的。 * 1、线程A持有一个 ThreadLocalMap 变量; * 2、线程A调用一个类的 ThreadLocal变量 tlA 的 get/set方法; * 3、tlA(ThreadLocal)的 get/set方法 获取当前线程A,调用 线程A 的 ThreadLocalMap变量 的get/put方法; * 4、其它线程 调用 tlA(ThreadLocal)的 get/set方法 同理。 */ ThreadLocal.ThreadLocalMap threadLocals; ThreadLocal.ThreadLocalMap inheritableThreadLocals; /** 线程栈的大小 */ private long stackSize; ... }

可以看到,Thread类持有两个ThreadLocal.ThreadLocalMap类型的成员变量:

  • threadLocals:每个线程自己的线程局部变量表,是ThreadLocal存取的主战场;
  • inheritableThreadLocals:可继承的线程局部变量表,用于子线程继承父线程的变量(后文详述)。

线程对象在初始化时这两个字段均为null,直到第一次调用某个ThreadLocalsetget时才被懒加载创建。ThreadLocal本身不存储任何数据,它只是"钥匙"——真正的"锁柜"是每个线程各自的ThreadLocalMap

ThreadLocal 核心 API 源码解析

有了上面的铺垫,ThreadLocal的源码就非常直观了。以下代码与注释来自仓库文档 ThreadLocal.md 的完整继承与解读。

set(T value):把值写入当前线程

public class ThreadLocal<T> { /** * ThreadLocal能为每个 Thread线程 绑定一个专属值的奥秘就是: * 每个Thread对象都持有一个 ThreadLocalMap类型的成员变量,其key为ThreadLocal对象, * value为绑定的值,所以每个线程调用 ThreadLocal对象 的set(T value)方法时,都会将 * 该ThreadLocal对象和绑定的值 以键值对的形式存入当前线程,这样,同一个ThreadLocal对象 * 就可以为每个线程绑定一个专属值咯。 * 每个线程调用 ThreadLocal对象的get()方法时,就可以根据 当前ThreadLocal对象 get到 绑定的值。 */ public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程对象中持有的 ThreadLocalMap类型的成员变量 // ThreadLocalMap,看名字也知道它是一个 Map类型的 类 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { // 经过前面对 Thread类 源码的分析,可以知道,Thread类中有一个 ThreadLocalMap 类型的 // threadLocals变量 return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ... }

set的完整调用链为:ThreadLocal.set(value)Thread.currentThread()拿到当前线程 →getMap(t)取出该线程的threadLocals→ 若map已存在则map.set(this, value),否则先通过createMap创建ThreadLocalMap并放入首个键值对。

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; } } return setInitialValue(); }

get的逻辑与set对称:先取当前线程的ThreadLocalMap,若 map 存在且能通过当前ThreadLocal对象命中对应的Entry,直接返回其value;若 map 不存在或没有命中,则调用setInitialValue()

setInitialValue()在 JDK 源码中的实现如下:它调用可被子类重写的initialValue()方法获取初始值(默认返回null),然后走与set相同的"创建/写入"路径:

private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; }

这就是为什么第一次get()未显式set时会返回null(或你重写initialValue()提供的默认值),同时它也会触发ThreadLocalMap的懒创建。

remove():显式解除绑定

public void remove() { // 获取当前线程的ThreadLocalMap成员变量,不为空就将当前 ThreadLocal对象 // 对应的 键值对 remove掉 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }

remove直接删除当前线程ThreadLocalMap中与该ThreadLocal对象对应的键值对。它是防止内存泄漏的关键手段,后文会重点强调其使用时机。

ThreadLocalMap:线程局部变量的"存储仓"

ThreadLocalMapThreadLocal的静态内部类,也是整个机制的核心。与大部分Map实现相同,它底层使用动态数组保存键值对Entry,同样具备rehashresize等操作,但它与HashMap有两个显著差异:key 被固定为ThreadLocal类型,且采用开放寻址法(线性探测)解决哈希冲突

Entry:弱引用 Key 的设计

static class ThreadLocalMap { /** * 存储键值对,key 为 ThreadLocal对象,value 为 与该ThreadLocal对象绑定的值 * Entry的key是对ThreadLocal的弱引用,当抛弃掉ThreadLocal对象时,垃圾收集器会 * 忽略这个key的引用而清理掉ThreadLocal对象,防止了内存泄漏 */ static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... }

Entry继承自WeakReference<ThreadLocal<?>>,即key(ThreadLocal 对象)是弱引用,value 是强引用。这样设计的好处是:当外部不再强引用某个ThreadLocal对象时,GC 可以回收该 key,避免ThreadLocal对象本身长期驻留内存。但代价也随之而来——value 仍然是强引用,只要线程(线程池中的线程)存活,即使 key 已被回收(e.get() == null),value 也不会被自动回收,从而形成"key 为 null 的陈旧条目",这就是 ThreadLocal 内存泄漏的根源。

哈希定位与线性探测

// 看过 HashMap 或 ConcurrentHashMap 源码的同学 一定下面对这些代码很眼熟 /** * 数组初始容量 */ private static final int INITIAL_CAPACITY = 16; /** * Entry数组,用于存储 <ThreadLocal<?> k, Object v>键值对 */ private Entry[] table; /** * Entry元素数量 */ private int size = 0; /** * 类似于 HashMap 扩容因子机制 */ private int threshold; // Default to 0 private void setThreshold(int len) { threshold = len * 2 / 3; } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }
  • 初始容量INITIAL_CAPACITY = 16
  • 负载因子阈值不是 0.75 而是2/3threshold = len * 2 / 3),比 HashMap 更"保守",这是为了给线性探测留出更多的空槽位、降低探测链长度;
  • nextIndex/prevIndex实现了环形数组遍历:到数组末尾时回绕到下标 0,这就是开放寻址法(线性探测)的"继续往后找"逻辑。

每个ThreadLocal对象都持有一个threadLocalHashCode,在 JDK 源码中它由静态AtomicInteger按固定增量0x61c88647(黄金分割数相关的斐波那契散列增量)递增生成,能保证哈希值在数组长度取模后分布足够均匀。定位公式为:

int i = key.threadLocalHashCode & (len - 1);

构造方法:首次创建的两种入口

/** * 系列构造方法 */ 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); } private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }

第一个构造方法用于线程首次set/get时创建,直接把首个键值对放入哈希位置。

第二个私有构造方法接收一个parentMap(父线程的inheritableThreadLocals),逐条复制父线程的键值对,并对每个值调用key.childValue(e.value)childValue的默认实现是原样返回,而InheritableThreadLocal重写了该方法,从而实现了子线程继承父线程线程局部变量的能力——这正是inheritableThreadLocals发挥作用的地方(详见下文)。

set():覆盖、替换陈旧条目与触发 rehash

/** * 常规Map实现类 的set()方法,只不过这里的 key被规定为 ThreadLocal类型 */ private void set(ThreadLocal<?> key, Object value) { 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(); // 如果key相等,覆盖value if (k == key) { e.value = value; return; } // 如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; // 若超过阀值,则rehash if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

set的核心流程:

  1. 从哈希位置开始线性探测:若遇到 key 相同的Entry,直接覆盖 value 并返回;
  2. 若遇到k == null的陈旧条目(key 已被 GC 回收),调用replaceStaleEntry用新键值对替换,同时清理探测链上的其他陈旧数据;
  3. 若探测一圈后找到空位,则放入新Entrysize自增;
  4. 若本次插入未能通过cleanSomeSlots(启发式清理)消除足够的陈旧条目,且size >= threshold,则触发rehash()

可见 ThreadLocal 在每次set时都会"顺手"做内存清理,这也是"key 弱引用 + 操作时清理"这套防泄漏机制的完整闭环。

getEntry() 与探测失败后的补救

/** * 根据 ThreadLocal对象 获取其对应的 Entry实例 */ 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); }

哈希位置直接命中(e.get() == key)时 O(1) 返回;否则调用getEntryAfterMiss沿着探测链继续查找,过程中若遇到e.get() == null的陈旧条目,同样会调用expungeStaleEntry(i)顺手清理——每次get也在做内存回收

remove():清除并整理

/** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

remove找到目标Entry后执行e.clear()(清掉弱引用 key),再调用expungeStaleEntry(i)清理该位置的陈旧条目并重整探测链,保证后续探测不被空洞打断。

rehash 与 resize:清理 + 扩容

/** * 调整当前table的容量。首先扫描整个容器,以删除过时的条目,如果这不能充分缩小表的大小, * 将进行扩容操作 */ private void rehash() { // 扫描整个容器,删除过时的条目 expungeStaleEntries(); // 若未能充分缩小表的大小,则进行扩容操作 if (size >= threshold - threshold / 4) resize(); } /** * 扩容为原容量的两倍 */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; // 遍历Entry[]数组 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); // 如果key=null,把value也置null,有助于GC回收对象 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; }
  • rehash先做全表expungeStaleEntries()清理陈旧条目;若清理后size仍达到threshold - threshold / 4(即阈值的 3/4),才执行resize
  • resize将容量翻倍newLen = oldLen * 2),遍历旧表重新哈希:陈旧条目直接把valuenull帮助 GC,有效条目按新长度重新线性探测入位;
  • HashMap的链表重排不同,ThreadLocalMap 扩容后所有有效条目都要按新容量重新探测放置,这也是后续 NettyFastThreadLocal想要规避的开销之一。

子线程传值:InheritableThreadLocal 与 Thread.init

前面提到Thread类还有一个inheritableThreadLocals字段。在 Thread 类源码分析 的init()方法中可以看到子线程是如何继承父线程变量的:

private void init(ThreadGroup threadgroup, Runnable runnable, String name, long l, AccessControlContext accesscontrolcontext) { ... // 当前线程就是该线程的父线程 Thread parent = currentThread(); ... target = runnable; setPriority(priority); if (parent.inheritableThreadLocals != null) // 创建线程共享变量副本 inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); stackSize = l; // 分配线程id tid = nextThreadID(); }

关键逻辑:

  1. 创建新线程时,currentThread()即为父线程;
  2. 父线程的inheritableThreadLocals不为 null,则调用ThreadLocal.createInheritedMap(parent.inheritableThreadLocals),内部就是走前文分析的ThreadLocalMap(ThreadLocalMap parentMap)私有构造方法,配合InheritableThreadLocal.childValue()完成值复制;
  3. 因此只有继承自InheritableThreadLocal的变量才能被子线程感知;普通ThreadLocal的数据放在threadLocals中,子线程无法访问。

这一点在框架中非常实用,例如需要在新建线程时自动携带主线程的 traceId、用户上下文等场景。但要注意:继承发生在线程创建的那一刻,之后父线程对InheritableThreadLocal的修改不会同步给已创建的子线程。

使用注意事项与实战规范

原文档在结尾给出了两条非常重要的使用铁律,这里结合源码展开说明。

1. ThreadLocal 不是用来解决线程安全问题的

ThreadLocal 不是用来解决线程安全问题的,多线程不共享,不存在竞争!其目的是使线程能够使用本地变量。

从源码可以清晰看到:set/get操作的数据都落在当前线程自己的ThreadLocalMap里,线程之间物理隔离,自然不存在共享与竞争,也就谈不上"解决线程安全问题"。如果你的业务对象本身需要被多个线程共享并修改,ThreadLocal 并不能替你保证线程安全——它只负责把数据"藏"到各线程自己的储物柜里。另外,ThreadLocal 变量本身(ThreadLocal 对象)是共享的,线程之间共享的是这把"钥匙",而不是柜子里的"值"。

2. 线程池场景必须 remove,否则可能内存泄漏或数据串扰

项目如果使用了线程池,那么线程回收后 ThreadLocal 变量要 remove 掉,否则线程池回收线程后,变量还在内存中,可能会带来意想不到的后果!

结合源码理解这背后的机制:

  • 线程池中的线程执行完任务后并不会销毁,而是回到池中等待复用;
  • 只要线程存活,它持有的ThreadLocalMap就存活;即使ThreadLocal的 key 已被弱引用回收,value 仍是强引用,无法被 GC 回收——这是内存泄漏的根源(需要线程存活 + key 被回收两个条件同时满足);
  • 更隐蔽的风险是数据串扰:同一个复用线程执行的下一个任务,可能"继承"上一个任务遗留的 ThreadLocal 值,读到过期甚至错误的数据。

正确的姿势是在每个业务处理结束点finally块或容器拦截器)显式调用remove()

try { UserContextHolder.set(currentUser); doBusiness(); } finally { UserContextHolder.remove(); // 无论业务成功失败,都要清理 }

对于 Tomcat 容器的线程池场景,原文档给出了经典方案:继承HandlerInterceptorAdapter,复写afterCompletion()方法完成清理

public class ThreadLocalCleanInterceptor extends HandlerInterceptorAdapter { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求处理完成后,清理当前线程(Tomcat 工作线程)上绑定的 ThreadLocal 变量, // 防止变量残留在线程池复用线程中,避免内存泄漏与后续请求的数据串扰 UserContextHolder.remove(); super.afterCompletion(request, response, handler, ex); } }

随后在 Spring MVC 配置中注册该拦截器即可。afterCompletion在整个请求处理链路(含异常)结束后被回调,正好对应 Tomcat 工作线程归还线程池的时机。(注:HandlerInterceptorAdapter在 Spring 5.3 起已标记为废弃,新项目可直接实现HandlerInterceptor接口并用其default方法,思路完全一致。)

延伸阅读:Netty FastThreadLocal 的优化思路

理解了原生 ThreadLocal 的"哈希 + 线性探测 + 扩容重哈希"实现后,就能更好地理解为什么 Netty 要自研 FastThreadLocal。

仓库中的FastThreadLocal源码分析.md指出,原生 ThreadLocal 的每次存取都要经历:计算threadLocalHashCode→ 与容量取模定位 → 若冲突则线性探测。而FastThreadLocal换了一条路:每个FastThreadLocal在构造时通过InternalThreadLocalMap.nextVariableIndex()从全局自增计数器拿到一个固定下标,存取时直接用该下标访问Object[] indexedVariables数组的对应位置:

public FastThreadLocal() { index = InternalThreadLocalMap.nextVariableIndex(); }

其性能优势正是针对原生 ThreadLocal 的三个开销点:

  • 定位更快:数组下标直接访问,免去了哈希计算与冲突探测;
  • 扩容更简单:数组扩容只需复制原内容并用占位对象填充,无需像 ThreadLocalMap 那样对全部有效条目重新哈希(仍可能二次冲突);
  • 遍历与回收更可控:所有FastThreadLocal的引用被统一保存在数组首位集合中,通过FastThreadLocal.removeAll()一次性清理全部变量,配合 NettyDefaultThreadFactory对任务执行完的自动清理(finally { FastThreadLocal.removeAll(); }),在避免内存泄漏的同时省去了原生 ThreadLocal 每次操作后的启发式清理开销。

这恰好从反面印证了原生 ThreadLocal 设计中"弱引用 + 操作时启发式清理"这套机制的成本与必要性。

总结

回到核心结论:

  1. ThreadLocal 本身不存数据,它只是 key;真正存储数据的ThreadLocalMap挂在每个ThreadthreadLocals字段上,天然实现线程隔离;
  2. set线性探测写入、get哈希定位读取、remove显式清除;底层是容量 16、阈值 2/3、冲突线性探测的动态Entry[]数组;
  3. Entry的 key 为弱引用,value 为强引用——这既是防泄漏设计,也是泄漏隐患的来源,因此每次 set/get/remove 都会伴随陈旧条目清理
  4. InheritableThreadLocal通过Thread.init()中的createInheritedMap实现子线程继承;
  5. 线程池 / Web 容器复用线程的场景下,务必在业务结束点remove(),这是源码层面可以明确推导出的硬性规范。

如果想继续深入,仓库还提供了关联的 Thread 类源码分析(理解threadLocals的完整上下文)与 Netty FastThreadLocal 源码分析(高性能线程局部变量的另类实现),两者对照阅读,对线程局部变量机制的理解会更立体。

【免费下载链接】source-code-hunter😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶,Mybatis、Netty、Dubbo 框架,及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 14:32:22

AI Agent多租户数据库选型:从VM级隔离到Serverless弹性实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 14:31:08

基于Spring Boot和MyBatis的实验教学管理系统设计与实现

简介&#xff1a;一套基于Java核心技术的实验教学管理系统完整源码&#xff0c;面向教育技术开发者、Java初学者及高校实验管理人员&#xff0c;覆盖用户管理、课程安排、实验报告提交等典型场景。资源包总文件131个&#xff0c;约1.95MB&#xff0c;包含73个Java后端源文件、1…

作者头像 李华
网站建设 2026/9/12 14:31:07

深度学习语音情绪识别:从特征工程到CNN模型部署实战

简介&#xff1a;基于深度学习语音情绪识别的完整工程包&#xff0c;面向毕业设计、课程设计及语音情感分析入门者&#xff0c;涵盖从音频特征提取、模型训练到预测推理的完整流程。包内共34个文件&#xff0c;其中17个Python脚本构成核心代码&#xff0c;包含训练/预测脚本、特…

作者头像 李华
网站建设 2026/9/12 14:26:53

POD-DMD实战:从CFD数据压缩到流场模态分析与短期预测

简介&#xff1a;这是一份面向流体力学与CFD研究者的POD-DMD模态分析资源包&#xff0c;聚焦计算流体动力学数据后处理中的降维与动态模式分解。内容围绕POD提取流场主导模态、DMD揭示时序演化特征展开&#xff0c;适用于航空航天、海洋工程、环境流体等方向的流场特征识别与机…

作者头像 李华