如何实现线程池之间的数据透传 ?

如何实现线程池之间的数据透传 ?

  • 引言
  • transmittable-thread-local
    • 概览
    • capture
      • 如何 capture
      • 如何保存捕获的数据
    • save 和 replay
    • restore
  • 小结


引言

当我们涉及到数据的全链路透传场景时,通常会将数据存储在线程的本地缓存中,如: 用户认证信息透传,链路追踪信息透传时;但是这里可能面临着数据在两个没有血缘关系的兄弟线程间透传的问题,这通常涉及到两个不同线程池之间数据的透传问题,如下图所示:

在这里插入图片描述
为了解决上面这个问题,最简单的思路就是手动在各个线程池的切换处添加捕获和回放逻辑,如下所示:

public class TTLMain {private static final Executor TTL_TEST_THREAD_POOL = Executors.newFixedThreadPool(1);public static void main(String[] args) {ThreadLocal<Integer> userId = new ThreadLocal<>();userId.set(1);// 1. 捕获当前线程的上下文信息Integer captured = userId.get();TTL_TEST_THREAD_POOL.execute(() -> {userId.set(2);// 2. 保存当前线程的上下文信息Integer backup = userId.get();// 3. 重放捕获的目标线程的上下文信息userId.set(captured);System.out.println("重放上下文后: 用户ID=" + userId.get());// 4. 恢复原先的线程上下文信息userId.set(backup);System.out.println("恢复上下文后: 用户ID="+userId.get());});}
}

其实不难看出整个处理过程分为四个阶段:

  1. capture : 捕获当前线程上下文信息
  2. save : 保存目标线程上下文信息
  3. replay : 重放当前线程的上下文信息到目标线程中
  4. restore : 恢复目标线程原先的上下文信息

整个过程属于一个模版流程,因此我们可以想办法把上面这段逻辑单独抽取固定下来,而非在各个切换处进行手动编码操作,因此这里引出了我今天想要介绍的现成工具类: transmittable-thread-local 。


transmittable-thread-local

transmittable-thread-local 是阿里开源的一个线程池间数据透传工具类,它的实现思路其实就是上面我讲的四个阶段,下面我们先来看看transmittable-thread-local具体是如何使用的吧:

public class TTLMain {private static ExecutorService TTL_TEST_THREAD_POOL = Executors.newFixedThreadPool(1);public static void main(String[] args) {demo1();demo2();demo3();}private static void demo1() {// 1. 修饰RunnableTransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);TTL_TEST_THREAD_POOL.execute(TtlRunnable.get(() -> {System.out.println("修饰Runnable: " + context.get());}));}@SneakyThrowsprivate static void demo2() {// 1. 修饰CallableTransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);Future<Integer> future = TTL_TEST_THREAD_POOL.submit(TtlCallable.get(context::get));System.out.println("修饰Callable: "+future.get());}@SneakyThrowsprivate static void demo3() {// 1. 修饰线程池TTL_TEST_THREAD_POOL = TtlExecutors.getTtlExecutorService(TTL_TEST_THREAD_POOL);TransmittableThreadLocal<Integer> context = new TransmittableThreadLocal<>();context.set(1);Future<Integer> future = TTL_TEST_THREAD_POOL.submit(context::get);System.out.println("修饰线程池: "+future.get());}
}

使用上比较简单,核心还是将capture,save,replay ,restore 四个阶段的逻辑以模版流程的形式安排到了TtlRunnable和TtlCallable中,下面我们就来看看transmittable-thread-local具体是如何实现的,以及我们能从中学到什么样设计技巧。


概览

在这里插入图片描述
TransmittableThreadLocal实现了InheritableThreadLocal,其可以确保数据能够在父子线程间进行透传,透传逻辑体现在Thread的构造函数中;而TransmittableThreadLocal要做的事情就是解决数据在不同线程池之间进行数据透传的问题,该问题解决思路就是本篇开头提到的思路,下面我将分四个阶段,依次来看看TransmittableThreadLocal是如何实现的。


capture

捕获阶段我们需要捕获当前线程使用到的所有TransmittableThreadLocal实例的数据,这一点如何做到 ? 以及我们用什么样的数据结构来保持捕获到的数据呢 ?

便于行文方便,下面会将TransmittableThreadLocal简写为TTL

如何 capture

如果当前线程本身没有向某个TTL实例中设置任何数据,那么其实没有必要捕获该实例内部的数据,因此这里只会在初次调用TTL的set方法时,才会向TTL内部的全局ThreadLocal注册表进行注册:

    // WeakHashMap 的key是被弱引用对象引用着的,并且value值允许为null,因此可以作为set集合使用// 这里就是当做Set集合使用的,因为这里我们只需要知道当前线程使用到的TTL有哪些private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {return new WeakHashMap<>();}@Overrideprotected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {return new WeakHashMap<>(parentValue);}};@Overridepublic final void set(T value) {if (!disableIgnoreNullValueSemantics && value == null) {// may set null to remove valueremove();} else {super.set(value);// 向全局ThreadLocal注册表进行注册addThisToHolder();}}private void addThisToHolder() {// 防止重复注册if (!holder.get().containsKey(this)) {// 进行注册,也就是添加到Set集合中holder.get().put((TransmittableThreadLocal<Object>) this, null);}}

holder的职责是负责记录当前线程使用到了哪些TTL,因此相对于TTL来说,Holder本身需要是全局静态的,同时又因为需要记录<线程,List< TTL >> 的映射关系,所以这里就有两种思路:

private static final Map<Thread,Set<TTL>> holder
or
private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder

这两种思路的前者就不多说了,很直接的思路,后者则是将当前线程使用到的TTL保存到了当前线程本地空间中,这样就避免了holder集合多线程情况下争用问题的发生:
在这里插入图片描述
这里比较有趣的一点在于为什么holder保存当前线程使用到的TTL时,需要使用WeakHashMap这样一个弱引用Map呢 ?这一点和ThreadLocalMap中的Entry弱引用实现一致,那么这两者之间是否存在使用场景上的联系呢?

在这里插入图片描述
ThreadLocalMap使用场景下,Table中的key类型为ThreadLocal,val类型为我们通过ThreadLocal设置到当前线程本地空间中的值,如果ThreadLocal对象的引用变量和创建都位于某个方法内部,那么该方法执行完毕后,ThreadLocal理应被回收,如下所示:

    private static void threadLocalGCTest(){ThreadLocal<Integer> tl = new ThreadLocal<>();tl.set(1);}

按理来说,如果tl对象实例占比大小不大,在经过逃逸分析后,会优先进行栈上分配,那么当栈帧被弹出时,该对象理应被直接回收掉,那么这里实际上并不会,因为什么呢?

因为当前线程的table中存在entry的key引用着当前tl对象 :

在这里插入图片描述
但是此时应用程序本身已经失去了对tl对象实例的引用,按照道理来说tl是需要被回收掉的,如果不回收,那就等于发生了内存泄漏,因此这里Entry本身就必须采用弱引用实现,这样才能在GC扫描到当前对象时,将当前tl对象实例进行回收。

对象只存在弱引用,说明对象目前只被弱引用对象实例所引用,软引用和虚引用含义也算如此,这一点弄清楚很重要。

从下图也能看出,但是val并没有被回收掉,严格来说也算是内存泄漏,只有等到当前线程的ThreadLocalMap后面get和set过程中,进行探测式清理和启发式清理时,才会被回收掉 :
在这里插入图片描述
这里还有一点需要注意,如果把上述案例改为如下示例,此时ThreadLocal并没有被当前线程所使用到,因此也就不会主动注册到当前线程内部的ThreadLocalMap中去,也就不存在ThreadLocalMap中的key对当前ThreadLocal实例的引用关系了:

    private static void threadLocalGCTest(){ThreadLocal<Integer> tl = new ThreadLocal<>();}

因此也就无需考虑回收问题了。


那为什么TTL要采用WeakHashMap来保存当前线程使用到的TTL实例呢?

在这里插入图片描述
这里原因其实是一致的,如果TTL对象实例丢失了应用程序的强引用关联,那么必须确保TTL能够被回收掉,具体场景还是如下所示:

在这里插入图片描述
此时TTL就不止当前线程ThreadLocalMap中key的弱引用了,还多了一个全局注册表的引用,所以必须将该全局注册表对象也设置为弱引用实现。


如何保存捕获的数据

第一个问题搞清楚了,下面来看第二个问题: 我们应该使用什么样的数据结构来保存被捕获的数据呢 ?

这个问题我们需要回到TtlRunnable的实现中来,在TtlRunnable的构造函数中执行了第一阶段的捕获任务:

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {// 执行一阶段捕获任务this.capturedRef = new AtomicReference<>(capture());this.runnable = runnable;this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}

capture是Transmitter类的静态构造方法,从类名不难猜测出,TTL使用该类来保存被捕获的数据,下面来看看它的capture方法实现:

        public static Object capture() {final HashMap<Transmittee<Object, Object>, Object> transmittee2Value = newHashMap(transmitteeSet.size());// 1. transmitteeSet 集合是什么呢 ? 为什么这里不直接从Holder集合取出当前线程使用到的所有TTL呢 ?for (Transmittee<Object, Object> transmittee : transmitteeSet) {// 2. transmittee2Value 为什么要保存这样的映射关系 ? transmittee.capture() 该方法捕获了什么数据呢 ?transmittee2Value.put(transmittee, transmittee.capture());...            }// 3. 这里返回的一定就是被捕获的数据了,那具体又是如何保存的呢?return new Snapshot(transmittee2Value);}

在阅读完上面这段代码后,相信大家应该都存在以上三个疑惑,那么下面我们就来一一探索一下吧。

首先,Transmittee类本身负责完成捕获,回放和恢复三件事情,如下图所示:

在这里插入图片描述

在Transmittee类初始化时,会向transmitteeSet集合中注册两个Transmittee对象实例,

        private static final Set<Transmittee<Object, Object>> transmitteeSet = new CopyOnWriteArraySet<>();static {registerTransmittee(ttlTransmittee);registerTransmittee(threadLocalTransmittee);}

ttlTransmittee 负责完成上图的捕获和回放过程,而因为只有TTL具备Transmittable的能力,所以为了让那些普通的ThreadLocal也能享受到Transmittable的能力,就有了threadLocalTransmittee。

关于threadLocalTransmittee这块不是重点,大家可以自行查看其实现,比较容易理解,所以就直接跳过了。

下面我们来看一下ttlTransmittee类的capture方法是如何从Holder中获取到当前线程所有的TTL,然后进行保存的:

        private static final Transmittee<HashMap<TransmittableThreadLocal<Object>, Object>, HashMap<TransmittableThreadLocal<Object>, Object>> ttlTransmittee =new Transmittee<HashMap<TransmittableThreadLocal<Object>, Object>, HashMap<TransmittableThreadLocal<Object>, Object>>() {@NonNull@Overridepublic HashMap<TransmittableThreadLocal<Object>, Object> capture() {// 1. 负责保存capture结果的集合final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = newHashMap(holder.get().size());// 2. 遍历Holder集合中保存的TTL,将其保存到capture集合中// 这里的映射关系为: <TTL,当前快照数据>for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {// 获取当前线程在当前TTL中保存的数据ttl2Value.put(threadLocal, threadLocal.copyValue());}return ttl2Value;}...}                 

capture 捕获得到的结果映射集合如下图所示:
在这里插入图片描述
当依次处理完所有Transmittee后,当前线程本时刻上下文快照数据会被保存到Snapshot对象中,然后返回给TtlRunnable对象保存:

        private static class Snapshot {final HashMap<Transmittee<Object, Object>, Object> transmittee2Value;public Snapshot(HashMap<Transmittee<Object, Object>, Object> transmittee2Value) {this.transmittee2Value = transmittee2Value;}}

在这里插入图片描述


save 和 replay

重放阶段我们需要将已经捕获到的之前线程的上下文快照重放到当前线程上下文中,重放前我们需要保存当前线程的上下文快照,以便执行完当前runnable任务后,进行恢复:

save和replay两阶段在TLL实现中是紧密相连的,因此TTL中把这两个阶段合二为一,统称为了replay,同时capture,replay,restore三个阶段也缩称为CRR。

    @Overridepublic void run() {// 1. 获取已经捕获到的之前线程的上下文快照final Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after run!");}// 2. 将之前线程的上下文快照重放到当前线程上下文中,同时返回当前线程上下文快照final Object backup = replay(captured);try {// 3. 执行目标任务runnable.run();} finally {// 4. 利用backup快照,恢复当前线程之前的上下文环境restore(backup);}}

本节我们重点关注replay方法的实现,该方法分为两个阶段:

  1. 保存当前线程上下文快照
  2. 应用之前线程的上下文快照
        public static Object replay(@NonNull Object captured) {// 1. 获取之前线程的快照数据final Snapshot capturedSnapshot = (Snapshot) captured;// 2. 该集合用于保存当前线程的快照数据final HashMap<Transmittee<Object, Object>, Object> transmittee2Value = newHashMap(capturedSnapshot.transmittee2Value.size());// 3. 遍历capturedSnapshotfor (Map.Entry<Transmittee<Object, Object>, Object> entry : capturedSnapshot.transmittee2Value.entrySet()) {// 4. 获取transmittee和其对应的快照数据Transmittee<Object, Object> transmittee = entry.getKey();Object transmitteeCaptured = entry.getValue();// 5. 调用transmittee的replay方法进行快照重放,同时返回当前线程的快照,然后保存到transmittee2Value中transmittee2Value.put(transmittee, transmittee.replay(transmitteeCaptured));...}// 6. 返回当前线程的上下文快照return new Snapshot(transmittee2Value);}

transmittee类的replay是快照保存和重放逻辑实现的关键点,下面我们一起来看看:

public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {// 1. 保存当前线程快照数据final HashMap<TransmittableThreadLocal<Object>, Object> backup = newHashMap(holder.get().size());// 2. 遍历当前线程Holder集合中每个TTLfor (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {// 3. 依次获取每个TTLTransmittableThreadLocal<Object> threadLocal = iterator.next();// 4. 保存当前线程的上下文快照backup.put(threadLocal, threadLocal.get());// 5. 这里是要用之前线程上下文数据覆盖掉当前线程整个上下文数据,所以这里要分为讨论// 当前线程使用到之前线程没用到的ttl,那么直接清空ttl中的数据// 当前线程使用到了之前线程用到的ttl,那么直接覆盖,覆盖逻辑在循环下面 if (!captured.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 6. 当前线程使用到了之前线程用到的ttl,那么使用captured进行覆盖setTtlValuesTo(captured);// 7. 目前runnable或者callable任务执行前,回调ttl对应的接口doExecuteCallback(true);// 8. 返回当前线程的上下文快照数据return backup;
}

在这里插入图片描述

        private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {// 遍历captured集合中所有ttlfor (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {// 取出ttlTransmittableThreadLocal<Object> threadLocal = entry.getKey();// 把ttl对应的快照值重新设置回ttl中,此时就相当于设置到了当前线程本地空间中threadLocal.set(entry.getValue());}}

整个save 和 replay的过程比较简单,我们下面进入restore环节。


restore

当执行完目标任务后,就需要将当前线程之前的上下文状态进行恢复了,整个过程其实和调用函数类似,由于通用寄存器只存在一套,所以调用过程中就需要把通用寄存器当前状态压入函数栈帧中保存,待函数返回时,再从栈帧中弹出恢复先前运行状态。

这里由于线程本地空间只有一套,所以也需要在任务执行完毕后,恢复原本的上下文环境:

    @Overridepublic void run() {// 1. 获取已经捕获到的之前线程的上下文快照final Object captured = capturedRef.get();if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {throw new IllegalStateException("TTL value reference is released after run!");}// 2. 将之前线程的上下文快照重放到当前线程上下文中,同时返回当前线程上下文快照final Object backup = replay(captured);try {// 3. 执行目标任务runnable.run();} finally {// 4. 利用backup快照,恢复当前线程之前的上下文环境restore(backup);}}

利用backup快照进行恢复的过程其实很简单,下面我们快速来过一遍:

        public static void restore(@NonNull Object backup) {// 1. 遍历backup快照中所有Transmitteefor (Map.Entry<Transmittee<Object, Object>, Object> entry : ((Snapshot) backup).transmittee2Value.entrySet()) {// 2. 获取Transmittee对应的HashMap,里面保存着<TTL,snapShot Data> Transmittee<Object, Object> transmittee = entry.getKey();Object transmitteeBackup = entry.getValue();// 3. 调用Transmittee的restore方法完成恢复过程transmittee.restore(transmitteeBackup);...}}

transmittee类的restore是上下文恢复的关键点,下面我们一起来看看:

                    @Overridepublic void restore(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> backup) {// 1. runnable方法调用后,执行TTL对应的回调doExecuteCallback(false);// 2. 遍历当前Holder集合中所有TTLfor (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {// 3. 获取到当前遍历的TTLTransmittableThreadLocal<Object> threadLocal = iterator.next();// 4. 将threadLocal中不存于backup的threadLocal都进行清空if (!backup.containsKey(threadLocal)) {iterator.remove();threadLocal.superRemove();}}// 5. 将backup中的TTL依次进行恢复,该方法上面介绍过,这里不再多说setTtlValuesTo(backup);}

在这里插入图片描述


小结

transmittable-thread-local 本身的设计思路不难理解,本文也只是针对TTL的核心流程源码进行了讲解,如果想进一步学习,可以自行拉取TTL项目源码进行学习。

TTL还提供了一种基于Java Agent的无侵入方案实现,感兴趣的小伙伴可以去 github 项目主页了解一波。

本文只是笔者个人观点,如果不正确的地方欢迎在评论区留言指出。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【自学开发之旅】Flask-会话保持-API授权-注册登录

http - 无状态-无法记录是否已经登陆过 #会话保持 – session cookie session – 保存一些在服务端 cookie – 保存一些数据在客户端 session在单独服务器D上保存&#xff0c;前面数个服务器A,B,C上去取就好了&#xff0c;业务解耦。—》》现在都是基于token的验证。 以上是基…

【vue+elementUI】输入框样式、选择器样式、树形选择器和下拉框样式修改

输入框样式、选择器样式和下拉框样式修改 1、输入框和选择器的样式修改&#xff1a;2、下拉弹框样式A. 选择器的下拉弹框样式修改B. 时间选择器的下拉弹框样式修改C. vue-treeselect树形下拉框样式 1、输入框和选择器的样式修改&#xff1a; 写在style中不能加scoped&#xff0…

手撸列表数据内嵌动态th甘特图

需求如图&#xff1a;日期为后端返回的七天日期&#xff0c;这七天组成由甘特图内嵌展示。 解决思路&#xff1a;这个vue项目中el-table自带样式过多&#xff0c;且不方便动态渲染数据&#xff0c;所以用div模拟了&#xff0c;这里甘特图精度为半天所以用v-if判断了&#xff0…

去耦电路设计应用指南(一)MCU去耦设计介绍

&#xff08;一&#xff09;MCU去耦设计介绍 1. 概述2. MCU需要去耦的原因2.1 去耦电路简介2.2 电源噪声产生的原因2.3 插入损耗2.4 去耦电路简介 参考资料来自网上&#xff1a; 1. 概述 我们经常看到单片机或者IC电路管脚常常会放置一个或者多个陶瓷电容&#xff0c;他们主要…

小红书AI绘画头像号,私域引流4000+人的暴力流量玩法

本期是赤辰第30期AI项目教程&#xff0c;底部准备了9月粉丝福利&#xff0c;可以免费领取。 今天给大家分享在小红书上强引流项目玩法&#xff1a;AI头像壁纸号&#xff0c;都知道&#xff0c;壁纸/头像/漫改&#xff0c;一直是蓝海项目&#xff0c;流量大且好变现&#xff0c;…

JavaScript - canvas - 放大镜

效果 示例 项目结构&#xff1a; 源码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>放大镜</title><style type"text/css">div {width: 200px;height: 200px;display: inline-bl…

C语言入门Day_24 函数与指针

目录 前言&#xff1a; 1.指针和数组 2.函数和指针 3.易错点 4.思维导图 前言&#xff1a; 我们知道数组是用来存储多个数据的&#xff0c;以及我们可以用指针来指向一个变量。那么我们可以用指针来指向一个数组中的数据么&#xff1f; 指针除了可以像指向一个变量一样指…

怎么快速提取图片中的文字信息?怎么使用OCR图片文字提取一键提取文字

图片里的文字如何提取?一些图片中的文字信息是我们需要的&#xff0c;但是一个个输入太麻烦了&#xff0c;怎么将图片上的文字提取出来?Initiator是一款易于使用的小型 macOS OCR&#xff08;光学字符识别&#xff09;应用程序&#xff0c;可提取和识别 Mac 计算机屏幕上的任…

电路的基本定律——基尔霍夫定律

基尔霍夫定律 &#x1f391;预备知识&#x1f391;基尔霍夫电流定律(KCL)&#x1f383;基尔霍夫电流定律的本质&#xff1a;节点上电荷具有连续性(不会突变)&#x1f383;基尔霍夫电流定律的推广&#xff1a; &#x1f391;基尔霍夫的电压定律(KVL)&#x1f383;基尔霍夫电压定…

Prompt

文章目录 ChatGPT Prompt Engineering for Developers(吴恩达)引言指南Principleprinciple 1 - Use delimitersprinciple 1 - Ask for structured outputprinciple 1 - Check whether conditions are satisfiedprinciple 1 - Few-shot promptingprinciple 2 - 指定完成任务所需…

全流程HEC-RAS 1D/2D水动力与水环境模拟技术案例实践及拓展应用丨从小白到精通,十九项案例实践

目录 专题一 水动力模型基础 专题二 恒定流模型(1D/2D) 专题三 一维非恒定流 专题四 二维非恒定流模型&#xff08;一&#xff09; 专题五 二维非恒定流模型&#xff08;二&#xff09; 专题六 HEC-RAS的水质模型 专题七 高级主题 水动力与水环境模型的数值模拟是实现水…

Go 围炉札记

文章目录 一、Go 安装 一、Go 安装 VScode下配置Go语言开发环境【2023最新】 基础篇&#xff1a;新手使用vs code新建go项目 vscode里安装Go插件和配置Go环境 Documentation Golang 配置代理 Go命令详解 一文详解Go语言常用命令 Go 语言教程 熬夜整理&#xff0c;最全的Go语…

数字经济水平测算(内含4种版本2种方式)-地级市(2011-2021年)

参照赵涛等&#xff08;2020&#xff09;的文章&#xff0c;利用熵值法和主成分对城市数字经济水平进行测算&#xff0c;包括原始数据及测算结果。内含4种版本2种方式&#xff0c;在8种情况下测算的数字经济水平。 一、数据介绍 数据名称&#xff1a;地级市-数字经济水平测算…

想要精通算法和SQL的成长之路 - 双指针【数组】

想要精通算法和SQL的成长之路 - 双指针【数组】 前言一. 合并两个有序数组二. 删除有序数组中的重复项 II 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 合并两个有序数组 原题链接 抓住重点信息&#xff1a; 两个数组都是非递减顺序排列。num1数组&#xff0c;末尾包…

[论文阅读]YOLOV1:You Only Look Once:Unified, Real-Time Object Detection

摘要 我们提出了YOLO&#xff0c;一种新的目标检测方法。之前的目标检测工作重新使用分类器来执行检测。相反&#xff0c;我们将目标检测表述为空间分离的边界框和相关类概率的回归问题。单个神经网络在一次评估中直接从完整图像中预测边界框和类别概率。由于整个检测管道是一…

Django之路由分发

1.include方法进行路由分发 在Django中&#xff0c;include函数用于将一个URL模式包含到另一个URL模式中&#xff0c;实现路由的分发。 一般时以includeapp的形式&#xff0c;将功能拆分不到不同的app中。 当使用include函数时&#xff0c;需要指定一个字符串参数&#xff0c;…

#循循渐进学51单片机#UART串口通信#not.10

1、能够理解UART串口通信的基本原理和通信过程。 1&#xff09;串行通信的初步认识 并行通信&#xff1a;通信时数据的各个位同时传送&#xff0c;可以实现字节为单位通信&#xff0c;但是通信线占用资源太多&#xff0c;成本高。 串行通信&#xff1a;一次只能发送一位&…

CTF 全讲解:[SWPUCTF 2021 新生赛]jicao

文章目录 参考环境题目index.phphighlight_file()include()多次调用&#xff0c;多次执行单次调用&#xff0c;单次执行 $_POST超全局变量HackBarHackBar 插件的获取 $_POST打开 HackBar 插件通过 HackBar 插件发起 POST 请求 GET 请求查询字符串超全局变量 $_GET JSONJSON 数据…

Lua学习笔记:词法分析

前言 本篇在讲什么 Lua的词法分析 本篇需要什么 对Lua语法有简单认知 对C语法有简单认知 依赖Visual Studio工具 本篇的特色 具有全流程的图文教学 重实践&#xff0c;轻理论&#xff0c;快速上手 提供全流程的源码内容 ★提高阅读体验★ &#x1f449; ♠ 一级标题…