`
yuwenlin2008
  • 浏览: 129439 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ThreadLocal详解

阅读更多

本篇介绍ThreadLocal以下三点:

1.ThreadLocal概述

2.ThreadLocal基本操作

3.ThreadLoad实现原理

 

一、ThreadLocal概述

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

举个例子,我出门需要先坐公交再做地铁,这里的坐公交和坐地铁就好比是同一个线程内的两个函数,我就是一个线程,我要完成这两个函数都需要同一个东西:公交卡(北京公交和地铁都使用公交卡),那么我为了不向这两个函数都传递公交卡这个变量(相当于不是一直带着公交卡上路),我可以这么做:将公交卡事先交给一个机构,当我需要刷卡的时候再向这个机构要公交卡(当然每次拿的都是同一张公交卡)。这样就能达到只要是我(同一个线程)需要公交卡,何时何地都能向这个机构要的目的。

有人要说了:你可以将公交卡设置为全局变量啊,这样不是也能何时何地都能取公交卡吗?但是如果有很多个人(很多个线程)呢?大家可不能都使用同一张公交卡吧(我们假设公交卡是实名认证的),这样不就乱套了嘛。现在明白了吧?这就是ThreadLocal设计的初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

 

二、ThreadLocal基本操作

public class ThreadLocalTest2 {

	private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};

	public static void main(String[] args) {
		for (int i = 0; i < 5; i++) {
			new Thread(new MyThread(i)).start();
		}
	}

	static class MyThread implements Runnable {
		private int index;

		public MyThread(int index) {
			this.index = index;
		}

		public void run() {
			System.out.println("线程" + index + "的初始value:" + value.get());
			for (int i = 0; i < 10; i++) {
				value.set(value.get() + i);
			}
			System.out.println("线程" + index + "的累加value:" + value.get());
		}
	}
}

执行结果为:

线程0的初始value:0
线程1的初始value:0
线程2的初始value:0
线程3的初始value:0
线程4的初始value:0
线程3的累加value:45
线程4的累加value:45
线程2的累加value:45
线程1的累加value:45
线程0的累加value:45 

可以看到,各个线程的value值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果。

 

三、ThreadLocal实现原理

我们先看看JDK8的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();
    }

其中getMap的源码:

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

setInitialValue函数的源码:

 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;
    }

createMap函数的源码:

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

简单解析一下,get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

然后需要注意的是Thread类中包含一个成员变量:

ThreadLocal.ThreadLocalMap threadLocals = null;

所以,可以总结一下ThreadLocal的设计思路:

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

这个方案刚好与我们开始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能,据说性能的提升不是一点两点(没有亲测)
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

再深入一点

先交代一个事实:ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
        ...
}

下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:


然后网上就传言,ThreadLocal会引发内存泄露,他们的理由是这样的:

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄露。

我们来看看到底会不会出现这种情况。 其实,在JDK的ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry方法的源码:

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);
        }

getEntryAfterMiss函数的源码:

 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;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

expungeStaleEntry函数的源码:

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

整理一下ThreadLocalMap的getEntry函数的流程:

  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询

在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。 但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的genEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。  

 

转自:https://www.zhihu.com/question/23089780

分享到:
评论

相关推荐

    电话销售技巧互联网销售技巧.ppt

    电话销售技巧互联网销售技巧.ppt

    基于主从博弈的共享储能与综合能源微网优化运行研究:情景四仿真复现 电热综合需求响应

    内容概要:本文探讨了综合能源微网与共享储能在现代能源环境中的创新性结合,重点研究了微网运营商与用户聚合商之间的博弈关系。通过主从博弈模型,微网运营商作为上层领导者制定价格策略,用户聚合商作为下层跟随者调整用能行为,而共享储能作为辅助设施优化微网运行。采用迭代式启发式算法和CPLEX求解器对博弈模型进行求解,实现了微网聚合商和用户聚合商利益的双赢。特别针对情景四(含共享储能和电制热设备)进行了仿真复现,展示了这种结合对微网运营和用户用能行为的具体影响。 适合人群:从事能源管理和微电网研究的专业人士,尤其是关注共享储能和博弈论应用的研究人员和技术人员。 使用场景及目标:适用于希望深入了解综合能源微网与共享储能结合机制的人群,目标是掌握主从博弈模型的应用,理解如何通过优化价格策略实现微网和用户利益的最大化。 其他说明:本文提供了详细的理论背景和仿真案例,有助于读者全面理解这一领域的最新进展和技术实现路径。

    智能截图工具V2.0源码-Java+JavaFX+Tesseract OCR文字识别完整项目

    基于Java 17+JavaFX开发的智能截图工具,集成Tesseract OCR引擎实现中英文文字识别。包含区域截图、实时OCR识别、图像预处理、多策略识别等功能。提供完整源码、Maven配置、语言包资源。识别准确率65-75%,适合学习OCR技术集成和JavaFX桌面应用开发。

    新型趋近律滑模控制在永磁同步电机(PMSM)中的应用研究与实现

    内容概要:本文详细介绍了新型趋近律滑模控制在永磁同步电机(PMSM)中的应用。首先阐述了PMSM的工作原理及其特点,强调了其高效、节能、稳定的优点。接着重点讲解了新型趋近律滑模控制的技术背景,指出这种控制方法相较于传统的滑模控制拥有更强的鲁棒性、抗干扰能力及适应性。文中还具体描述了该控制策略的实现步骤,包括算法设计、控制器实现和仿真验证三个部分。最后探讨了该技术的应用场景,特别是在高精度控制、节能减排和提升系统稳定性方面的潜力。 适合人群:从事电机控制系统研究的专业人士、高校相关专业师生、对先进电机控制技术感兴趣的工程技术人员。 使用场景及目标:适用于希望深入了解和掌握新型趋近律滑模控制技术的研究人员和技术开发者,旨在为他们提供理论依据和技术指导,帮助他们在实际项目中应用这一先进技术。 其他说明:文章不仅提供了详细的理论分析,还有具体的实现路径和案例分析,有助于读者全面理解和实践该技术。

    封装高复用Java线程池工具类

    封装高复用Java线程池工具类,详解参数模板、异常处理与优雅关闭,并提供完整代码,告别重复配置与资源泄漏。

    罗特曼透镜设计与HFSS链接 matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    西门子PLC程序集合:触摸屏、IO、电气图纸与雅马哈机器人集成 - 清晰结构的大型产线参考程序

    内容概要:本文详细介绍了西门子1200 PLC在大型产线控制系统中的应用实践。主要内容涵盖程序设计与硬件配置、莫风扇定子线端程序设计、两台PLC的编程与交互、设备集成与交互、程序结构与调试以及三方设备集成与调试六个方面。文中强调了通过合理的程序设计和硬件配置,实现了对莫风扇定子线端的精确控制以及其他设备的无缝集成和交互。程序设计采用了模块化编程方式,确保了程序的清晰性和可读性,在调试阶段通过模拟实际工作场景来测试程序的稳定性和可靠性。 适合人群:从事工业自动化领域的电气工程师和技术人员,尤其是那些希望深入了解西门子PLC编程及其在复杂产线环境中的应用的人群。 使用场景及目标:适用于正在规划或优化大型产线控制系统的工程师们,旨在提供一套完整的解决方案,从硬件选型到软件编程,再到最终的系统集成与调试,帮助他们提高产线的生产效率和产品质量。 其他说明:随着工业自动化技术的发展,未来PLC控制系统将朝着更加智能化和高效化的方向发展。本文提供的实践经验可以为相关从业者提供宝贵的参考资料。

    COMSOL电弧放电模型:基于磁流体方程的耦合电磁热流体与电路多物理场模拟 核心版

    内容概要:本文详细介绍了COMSOL电弧放电模型,重点探讨了基于磁流体方程的多物理场耦合模拟。电弧放电作为一种常见于高电压和强电流应用场景的现象,涉及到电磁、热、流体以及电路等多个物理场的复杂交互。COMSOL软件利用磁流体方程精确模拟电弧的形态和演变规律,通过多物理场耦合计算,提供了一种全面的模拟手段。文中还讨论了计算过程中面临的挑战,如复杂的物理场耦合、高难度的数值计算,并提出了相应的解决策略,包括高效求解器的设计和模型验证与校准。 适合人群:从事电弧放电研究及相关应用领域的科研人员和技术工程师。 使用场景及目标:适用于需要深入了解电弧放电现象背后的物理机制,以及希望通过多物理场耦合模拟提升相关设备性能的研究人员。目标是帮助读者掌握COMSOL电弧放电模型的基本原理和应用技巧,从而更好地应用于实际工程项目中。 其他说明:虽然文中未展示具体代码实现,但强调了理论背景和实际操作中的关键点,有助于指导后续的具体实施。

    葡萄藤叶图像数据集-zip

    葡萄藤的主要产品是新鲜或加工的葡萄。此外,葡萄叶作为副产品每年收获一次。葡萄叶的种类在价格和口味方面很重要。在这项研究中,通过使用葡萄藤叶子的图像进行基于深度学习的分类。为此,使用特殊的自发光系统拍摄了属于 5 个物种的 500 片藤叶的图像。后来,这个数字通过数据增强方法增加到 2500 个。该分类是使用最先进的 CNN 模型微调 MobileNetv2 进行的。作为第二种方法,从预先训练的 MobileNetv2 的 Logits 层中提取特征,并使用各种 SVM 内核进行分类。作为第三种方法,通过卡方方法选择从 MobileNetv2 的 Logits 层中提取的 1000 个特征,并将其减少到 250 个。然后,使用所选特征对各种 SVM 内核进行分类。最成功的方法是从 Logits 层中提取特征并使用卡方方法减少特征。最成功的 SVM 内核是 Cubic。该系统的分类成功率被确定为 97.60%。据观察,尽管分类中使用的特征数量减少,但特征选择提高了分类成功率。

    小波变换在图像处理中的Matlab实战应用:增强、融合、压缩与数字水印技术

    内容概要:本文详细介绍了小波变换的基本原理及其在图像处理领域的多种应用。首先解释了小波变换作为一种强大的时频局部化分析工具的工作机制,然后通过六个具体案例——基于小波塔式分解的图像增强、基于离散小波变换的图像融合、图像压缩和数字水印技术,展示了小波变换的实际应用场景。每个案例均配有详细的Matlab代码,帮助读者理解并实践小波变换的技术细节。 适合人群:对数字信号处理和图像处理感兴趣的初学者,尤其是希望深入了解小波变换理论及其应用的研究人员和技术爱好者。 使用场景及目标:①掌握小波变换的基础理论;②学会用Matlab实现小波变换的各种图像处理任务;③理解小波变换在图像增强、融合、压缩和数字水印方面的具体应用方法。 其他说明:文章不仅提供了理论讲解,还附有大量实用的Matlab代码片段,便于读者动手实践,加深理解。

    在面积为 x x 米的矩形网格上模拟地震测量Matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    第三章软件需求分析.ppt

    第三章软件需求分析.ppt

    【Tomcat配置】macOS系统下Tomcat安装与配置全流程:JDK环境搭建、Web应用部署及开机自启设置

    内容概要:本文档详细介绍了在macOS系统上安装和配置Tomcat服务器的步骤。首先,确保JDK已正确安装并配置环境变量,包括安装最新版本的JDK(如Oracle JDK或Azul Zulu JDK),并通过命令行验证JDK版本。接着,文档讲解了两种安装Tomcat的方法:使用Homebrew自动化安装或手动下载并解压Tomcat压缩包,同时配置Tomcat环境变量(如CATALINA_HOME)。此外,文档还涵盖了启动和停止Tomcat、验证安装是否成功的操作。对于Web应用部署,提供了直接部署WAR包和配置虚拟目录两种方式。最后,文档介绍了配置Tomcat管理界面及实现开机自启的方法,以及常见的故障排除技巧,如端口冲突、权限拒绝和管理页面无法访问等问题的解决方案。 适合人群:具备一定Linux命令行操作基础,熟悉Java开发环境,特别是对在macOS上搭建Java Web开发环境感兴趣的开发人员和技术爱好者。 使用场景及目标:①帮助用户快速在macOS上完成Tomcat服务器的安装与基本配置;②掌握部署Web应用的基本方法,包括直接部署WAR包和配置虚拟目录;③学习如何配置Tomcat管理界面,实现服务的开机自启,并解决常见问题。 阅读建议:由于涉及到具体的命令行操作和配置文件编辑,建议读者按照文档步骤逐一操作,确保每一步都正确无误。同时,对于遇到的问题,可以参考文档提供的解决方案或查阅官方文档进一步了解。

    电子商务考证练习题和参考答案.doc

    电子商务考证练习题和参考答案.doc

    电力电子学中非线性PID控制的Buck-Boost变换器优化设计与性能分析

    内容概要:本文详细介绍了将非线性PID控制应用于Buck-Boost变换器的设计与性能优化。首先,文章解释了非线性PID控制的概念及其相对于传统PID控制的优势,特别是在应对快速变化或非线性负载的情况。接着,文中具体阐述了TD非线性跟踪微分器的作用以及其在方波信号跟踪中的优异表现。随后,通过一系列仿真实验展示了非线性PID控制下的Buck-Boost变换器在不同条件下的稳定性和响应速度,如输入电压为20V时,输出电压能够迅速并稳定地达到设定值10V,且无超调现象。此外,文章还讨论了该控制方法在其他主电路上的应用潜力,强调了其广泛的适用性和灵活性。最后,作者指出这种新型控制策略在电力电子领域的广阔发展前景。 适合人群:从事电力电子研究的技术人员、高校相关专业师生、对电力电子控制系统感兴趣的工程技术人员。 使用场景及目标:适用于需要提高DC-DC转换器性能的研究项目和技术改造,旨在提升系统的响应速度、稳定性和鲁棒性。 其他说明:文章提供了详细的理论背景和实证数据支持,有助于读者深入理解非线性PID控制的工作机制及其实际应用效果。

    西门子1500 PLC博途程序实例:大型汽车焊装自动生产线程序硬件结构详解 · PLC编程 说明

    内容概要:本文详细介绍了基于西门子1500PLC的汽车焊装生产线自动化控制系统的设计与实现。涵盖了硬件连接(如PLC、触摸屏、智能终端、机器人)、智能通信(Profinet、IO-Link、S7协议)以及高级算法的应用。具体包括设备命名规范、速度监测算法、机器人通信配置、GRAPH顺控程序、安全程序设计、MES系统通信等内容。文中还提供了具体的编程示例,展示了如何通过SCL、梯形图和STL等多种编程方式实现复杂的功能。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC编程和自动化生产线有研究兴趣的人群。 使用场景及目标:适用于希望深入了解PLC编程、智能通信技术和自动化生产线集成的技术人员。目标是帮助读者掌握大型自动化项目的架构设计、编程技巧和故障排查方法。 其他说明:本文不仅提供理论知识,还包括大量实际操作经验分享,有助于提高读者的实际动手能力和解决问题的能力。

    电力电子学中开关电容多电平变换器的MatlabSimulink仿真及其电路拓扑结构与调制技术的应用

    内容概要:本文深入介绍了开关电容多电平变换器(SMLC)的基本概念、工作原理及其在电力电子学中的重要性。文中详细讲解了利用Matlab/Simulink进行SMLC仿真的步骤,涵盖多种电路拓扑结构(如二极管钳位型、飞跨电容型和级联H桥型),并重点讨论了载波层叠调制与逻辑组合脉冲触发技术的具体实现方法。此外,还提供了实践操作指南,包括搭建电路模型、配置参数、编写触发逻辑、运行仿真和分析结果等环节。 适合人群:从事电力电子领域的研究人员和技术人员,尤其是对开关电容多电平变换器感兴趣的工程技术人员。 使用场景及目标:适用于需要理解和掌握SMLC工作原理及其仿真方法的研究人员和技术人员。目标是通过详细的理论讲解和实践操作指导,使读者能够熟练运用Matlab/Simulink进行SMLC仿真,进而优化电力电子设备的设计和性能。 其他说明:本文不仅提供了理论知识,还附带了具体的实践操作步骤和代码片段,有助于读者快速上手并应用于实际项目中。

    Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明

    Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明,个人经导师指导并认可通过的高分设计项目,评审分99分,代码完整确保可以运行,小白也可以亲自搞定,主要针对计算机相关专业的正在做大作业的学生和需要项目实战练习的学习者,可作为毕业设计、课程设计、期末大作业。 Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测项目源码+文档说明Python基于KNN与线性回归的鲍鱼年龄预测

    基于STM32F407的认字图形游戏

    基于STM32F407的认字图形游戏

Global site tag (gtag.js) - Google Analytics