JVM内存泄露的ThreadLocal详解

目录

一、为什么要有ThreadLocal

二、ThreadLocal的使用

三、实现解析

实现分析

具体实现

Hash冲突的解决

开放定址法

链地址法

再哈希法

建立公共溢出区

四、引发的内存泄漏分析

内存泄漏的现象

分析

总结

错误使用ThreadLocal导致线程不安全


一、为什么要有ThreadLocal

先来看看一段最纯粹的原生JDBC代码

可以看到,在使用JDBC时,我们首先要配置后再拿到JDBC连接,然后在增删改查的业务方法中再次拿到这个连接,并把我们的SQL语句交给JDBC连接发送到真实的DB上执行。

在实际的工作中,我们不会每次执行SQL语句时临时去建立连接,而是会借助数据库连接池,同时因为实际业务的复杂性,为了保证数据的一致性,我们还会引入事务操作,于是上面的代码就会变成:

但是上面的代码包含什么样的问题呢?分析代码我们可以发现,执行业务方法business时,为了启用事务,我们从数据库连接池中拿了一个连接,但是在具体的insert方法和getAll方法中,在执行具体的SQL语句时,我们又从数据库连接池中拿另一个连接,这就说执行事务和执行SQL语句完全是不同的数据库连接,这会导致什么问题?事务失效了!!数据库执行事务时,事务的开启和提交、语句的执行等都是必须在一个连接中的。实际上,上面的代码要保证数据的一致性,就必须要启用分布式事务。

怎么解决这个问题呢?有一个解决思路是,把数据库连接作为方法的参数,在方法之间进行传递,比如下面这样:

但是我们分析平时我们使用SSM的代码会发现,我们在编写数据访问相关代码的时候从来没有把数据库连接作为方法参数进行传递。这意味着,对Spring来说,在帮我们进行事务托管的时候,会遇到同样的问题,那么Spring是如何解决这个问题的?

其实稍微分析下Spring的事务管理器的代码就能发现端倪,在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin中,我们会看到如下代码

上面的注释说明了“绑定连接到这个线程”,如何绑定的?继续深入看看

看来,Spring是使用一个ThreadLocal来实现 “绑定连接到线程”的。

现在我们可以对ThreadLocal下一个比较确切的定义了

此类提供线程局部变量。这些变量与普通对应变量的不同之处在于,访问一个变量的每个线程(通过其 get 或 set 方法)都有自己独立初始化的变量副本。ThreadLocal 实例通常是希望将状态与线程(例如,用户 ID 或事务 ID)相关联的类中的私有静态字段。

也就是说ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。由此也可以看出ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的差别。

synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问ThreadLocal则是副本机制。此时不论多少线程并发访问都是线程安全的。

ThreadLocal的一大应用场景就是跨方法进行参数传递,比如Web容器中,每个完整的请求周期会由一个线程来处理。结合ThreadLocal再使用Spring里的IOC和AOP,就可以很好的解决我们上面的事务的问题。只要将一个数据库连接放入ThreadLocal中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal获得就行了。

再比如,在微服务领域,链路跟踪中的traceId传递也是利用了ThreadLocal。

二、ThreadLocal的使用

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

• void set(Object value) 设置当前线程的线程局部变量的值。

• public Object get() 该方法返回当前线程所对应的线程局部变量。

• public void remove() 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

• protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

三、实现解析

实现分析

怎么实现ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易的方式就是用一个Map将线程的副本存放起来,Map里key就是每个线程的唯一性标识,比如线程ID,value就是副本值,实现起来也很简单:

/**
 * 类说明:自己实现的ThreadLocal
 */
public class MyThreadLocal<T> {
    //存放变量副本的map容器,以Thread为键,变量副本为value
    private Map<Thread,T> threadTMap = new HashMap<>();

    public synchronized T get(){
        return  threadTMap.get(Thread.currentThread());
    }

    public synchronized void set(T t){
        threadTMap.put(Thread.currentThread(),t);
    }
}

考虑到并发安全性,对数据的存取用synchronize关键字加锁,但是DougLee在《并发编程实战》中为做过性能测试

可以看到ThreadLocal的性能远超类似synchronize的锁实现ReentrantLock,比AtomicInteger也要快很多,即使我们把Map的实现更换为Java中专为并发设计的ConcurrentHashMap也不太可能达到这么高的性能。

怎么样设计可以让ThreadLocal达到这么高的性能呢?

最好的办法则是让变量副本跟随着线程本身,而不是将变量副本放在一个地方保存,这样就可以在存取时避开线程之间的竞争。

同时,因为每个线程所拥有的变量的副本数是不定的,有些线程可能有一个,有些线程可能有2个甚至更多,则线程内部存放变量副本需要一个容器,而且容器要支持快速存取,所以在每个线程内部都可以持有一个Map来支持多个变量副本,这个Map被称为ThreadLocalMap。

具体实现

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是一个声明在ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员变量,也就是ThreadLocalMap实例化是在Thread内部,所以getMap是直接返回Thread的这个成员。

看下ThreadLocal的内部类ThreadLocalMap源码,这里其实是个标准的Map实现,内部有一个元素类型为Entry的数组,用以存放线程可能需要的多个副本变量。

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。

get方法(如上图),其实就是拿到每个线程独有的ThreadLocalMap,然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行map的创建,初始化等工作。

Hash冲突的解决

HashMap:在 Java 的 HashMap 实现中,当发生哈希冲突(即多个键映射到了同一个哈希桶)时,采用的解决方法是使用链表结构来存储冲突的键值对。具体来说,每个哈希桶实际上是一个链表的头节点,当发生哈希冲突时,新的键值对会添加到该链表的末尾。这样,相同哈希值的键值对都会存储在同一个哈希桶对应的链表中。在 JDK 8 及之前版本,HashMap 使用的是单向链表来解决哈希冲突,但从 JDK 8 开始,当同一个哈希桶的链表长度超过一定阈值(TREEIFY_THRESHOLD,默认为8)时,会将链表转换为红黑树(自平衡二叉搜索树),以提高查找效率。

ThreadLocalMap:ThreadLocalMap 是 Java 中用于实现线程本地变量的一种机制。每个线程都拥有自己独立的 ThreadLocalMap,用于存储线程本地的变量值。在 ThreadLocalMap 的实现中,当一个 ThreadLocal 变量被创建并初始化后,其实际上作为键被存储在 ThreadLocalMap 中。ThreadLocalMap 使用线程自己的哈希码(Thread.currentThread())来决定存储位置。而对应的线程本地变量值则作为 ThreadLocalMap 中的值。

什么是Hash,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值,输入的微小变化会导致输出的巨大变化。所以Hash常用在消息摘要或签名上,常用hash消息摘要算法有:

(1)MD4

(2) MD5它对输入仍以512位分组,其输出是4个32位字的级联

(3)SHA-1及其他。

Hash转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。比如有10000个数放到100个桶里,不管怎么放,有个桶里数字个数一定是大于2的。

所以Hash简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。常用HASH函数:直接取余法、乘法取整法、平方取中法。 Java里的HashMap用的就是直接取余法。

我们已经知道Hash属于压缩映射,一定能会产生多个实际值映射为一个Hash值的情况,这就产生了冲突,常见处理Hash冲突方法:

开放定址法

基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不同又可以分为线性探测再散列、二次探测再散列、伪随机探测再散列

线性探测再散列即依次向后查找,二次探测再散列,即依次向前后查找,增量为1、2、3的二次方,伪随机,顾名思义就是随机产生一个增量位移。

ThreadLocal里用的则是线性探测再散列

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。Java里的HashMap用的就是链地址法,为了避免hash 洪水攻击,1.8版本开始还引入了红黑树。

再哈希法

这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

四、引发的内存泄漏分析

引用

Object o = new Object();

这个o,我们可以称之为对象引用,而new Object()我们可以称之为在内存中产生了一个对象实例。

当写下 o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。

强引用:就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

软引用:是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

内存泄漏的现象

事先准备好测试程序,并将堆内存大小设置为-Xmx256m,

启用一个线程池,大小固定为5个线程

场景1,首先任务中不执行任何有意义的代码,当所有的任务提交执行完成后,可以看见,我们这个应用的内存占用基本上为25M左右(使用的是java jdk 自带的jvisualvm命令工具)

场景2,然后我们只简单的在每个任务中new出一个数组,执行完成后我们可以看见,内存占用基本和场景1同

场景3,当我们启用了ThreadLocal以后:(ThreadLocalMap#static class Entry extends WeakReference<ThreadLocal<?>>弱引用,导致内存泄漏)

执行完成后我们可以看见,内存占用变为了100多M

场景4,于是,我们加入一行代码,再执行,看看内存情况:

可以看见,内存占用基本和场景1同。

这就充分说明,场景3,当我们启用了ThreadLocal以后确实发生了内存泄漏。

分析

根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 key 的,弱引用的对象在 GC 时会被回收。

因此使用了ThreadLocal后,引用链如图所示

图中的虚线表示弱引用。

这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry[] -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。

只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

所以回到我们前面的实验场景,场景3中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。

其实考察ThreadLocal的实现,我们可以看见,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法。

从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:

为什么使用弱引用而不是强引用?下面分两种情况讨论:

key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。

key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

总结

  • JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
  • JVM利用调用remove、get、set方法的时候,回收弱引用。
  • 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
  • 使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。

错误使用ThreadLocal导致线程不安全

/*
*类说明:错误使用示例
*/
public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);
    public static ThreadLocal<Number> value = new ThreadLocal<Number>()/*{
        @Override
        protected Number initialValue() {
            return new Number(0);
        }
    }*/;

    public void run() {
        Random r = new Random();
        //Number number = value.get();
        //每个线程计数加随机数
        number.setNum(number.getNum()+r.nextInt(100));
        //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

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

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}

//运行结果
Thread-4=206
Thread-0=206
Thread-1=206
Thread-3=206
Thread-2=206

为什么每个线程都输出115?难道他们没有独自保存自己的Number副本吗?为什么其他线程还是能够修改这个值?仔细考察ThreadLocal和Thead的代码,我们发现ThreadLocalMap中保存的其实是对象的一个引用,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果。

而上面的程序要正常的工作,应该的用法是让每个线程中的ThreadLocal都应该持有一个新的Number对象。

/*
*类说明:正确使用示例
*/
public class ThreadLocalUnsafe implements Runnable {

    //public static Number number = new Number(0);
    public static ThreadLocal<Number> value = new ThreadLocal<Number>(){
        @Override
        protected Number initialValue() {
            return new Number(0);
        }
    };

    public void run() {
        Random r = new Random();
        Number number = value.get();
        //每个线程计数加随机数
        number.setNum(number.getNum()+r.nextInt(100));
        //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

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

// 运行结果
Thread-3=92
Thread-0=69
Thread-1=88
Thread-4=9
Thread-2=70

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32F103RB多通道ADC转换功能实现(DMA)

目录 概述 1 硬件 1.1 硬件实物介绍 1.2 nucleo-f103rb 1.3 软件版本 2 软件实现 2.1 STM32Cube配置参数 2.2 项目代码 3 功能代码实现 3.1 ADC功能函数 3.2 函数调用 4 测试 4.1 DMA配置data width&#xff1a;byte 4.2 DMA配置data width&#xff1a;Half wor…

javascript DOM BOM 笔记

Web API API的概念 API&#xff08;Application Programming Interface,应用程序编程接口&#xff09;是一些预先定义的函数&#xff0c;目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力&#xff0c;而又无需访问源码&#xff0c;或理解内部工作机制的细…

使用esptool工具备份ESP32的固件(从芯片中备份下来固件)

本文以Windows电脑为例 板子为esp32-c3 1下载python 可在官网中下载,此处不进行讲解 使用如下代码查看是否安装了 Python&#xff08;终端输入&#xff09; python 2下载esptool 在终端输入如下代码即可下载 使用 pip&#xff08;推荐&#xff09;: 在你已经安装的 Pyth…

【大模型LLM面试合集】大语言模型架构_layer_normalization

2.layer_normalization 1.Normalization 1.1 Batch Norm 为什么要进行BN呢&#xff1f; 在深度神经网络训练的过程中&#xff0c;通常以输入网络的每一个mini-batch进行训练&#xff0c;这样每个batch具有不同的分布&#xff0c;使模型训练起来特别困难。Internal Covariat…

妙笔生词智能写歌词软件:创新助力还是艺术之殇?

在音乐创作日益普及和多样化的当下&#xff0c;各种辅助工具层出不穷&#xff0c;妙笔生词智能写歌词软件便是其中之一。那么&#xff0c;它到底表现如何呢&#xff1f; 妙笔生词智能写歌词软件&#xff08;veve522&#xff09;的突出优点在于其便捷性和高效性。对于那些灵感稍…

软件许可证优化怎么做最好!

在当今数字化发展的浪潮中&#xff0c;软件许可证的优化成为了 IT 总监们面临的一项重要挑战。在许可数量受限的情况下&#xff0c;如何将现有许可发挥最大利用率&#xff0c;是一个亟待解决的问题。 信息采集是优化的基础。 我们需要采集关于软件使用频率、使用时长、用户部门…

Python学习中使用循环(for, while)

在Python编程语言中&#xff0c;循环是一个非常重要的概念&#xff0c;可以帮助我们在代码中重复执行某些操作。Python支持两种主要的循环结构&#xff1a;for 循环和 while 循环。 1. for 循环 for 循环用于遍历一个序列&#xff08;如列表、元组、字符串&#xff09;或其他…

Linux 一键部署Mysql 8.4.1 LTS

mysql 前言 MySQL 是一个基于 SQL(Structured Query Language)的数据库系统,SQL 是一种用于访问和管理数据库的标准语言。MySQL 以其高性能、稳定性和易用性而闻名,它被广泛应用于各种场景,包括: Web 应用程序:许多动态网站和内容管理系统(如 WordPress)使用 MySQL 存…

STM32学习历程(day6)

EXTI外部中断使用教程 首先先看下EXTI的框图 看这个框图就能知道要先初始化GPIO外设 那么和前面一样 1、先RCC使能时钟 2、配置GPIO 选择端口为输入模式&#xff0c; 3、配置AFIO&#xff0c;选择我们用的GPIO连接到后面的EXTI 4、配置EXTI&#xff0c;选择边沿触发方式…

【Linux】升级FastJSON版本-jar

摘要 在长期运行的应用服务器上&#xff0c;近期的安全漏洞扫描揭示了fastjson组件存在潜在的安全隐患&#xff08;FastJSON是一个Java 语言实现的 JSON 解析器和生成器。FastJSON存在远程代码执行漏洞&#xff0c;恶意攻击者可以通过此漏洞远程执行恶意代码来入侵服务器&…

iOS 开发技巧 - 使用本地 json 文件

前言 使用本地 json 文件的场景&#xff0c;在我们开发功能的阶段&#xff0c;服务端接口字段定义好了后&#xff0c;有些接口响应很慢&#xff0c;请求到响应可能要 几十秒甚至一分钟&#xff0c;我们需要频繁调用接口来调试功能&#xff1b;还有就是调用一些我们需要付费的三…

coze搭建工作流和Agent

coze搭建工作流和Agent Agent LLM 记忆感知规划使用工具 LLM是大语言模型&#xff0c;prompt提示词影响LLM的输出质量 描述需求——>背景——>解决思路&#xff0c;提示词文档。 当有明确的需求和实现需求的路径时&#xff0c;可以通过搭建工作流来完成标准化任务为…

昇思25天学习打卡营第16天|应用实践之Vision Transformer图像分类

基本介绍 今天同样是图像分类任务&#xff0c;也更换了模型&#xff0c;使用的时候计算机视觉版的Transformer&#xff0c;即Vision Transformer&#xff0c;简称ViT。Transformer本是应用于自然语言处理领域的模型&#xff0c;用于处理语言序列&#xff0c;而要将其应用于图像…

CentOS6用文件配置IP模板

CentOS6用文件配置IP模板 到 CentOS6.9 , 默认还不能用 systemctl , 能用 service chkconfig sshd on 对应 systemctl enable sshd 启用,开机启动该服务 ### chkconfig sshd on 对应 systemctl enable sshd 启用,开机启动该服务 sudo chkconfig sshd onservice sshd start …

三级_网络技术_12_路由设计技术基础

1.R1、R2是一个自治系统中采用RIP路由协议的两个相邻路由器&#xff0c;R1的路由表如下图(a)所示&#xff0c;当R1收到R2发送的如下图(b)的(V.D)报文后&#xff0c;R1更新的4个路由表项中距离值从上到下依次为0、3、3、4 那么&#xff0c;①②③④可能的取值依次为()。 0、4、…

LaySNS模板仿RiPro日主题素材源码资源下载响应式CMS模板

该主题是网上泛滥的RiPro主题仿制而成的laysns模板&#xff0c;原主题是很强大的&#xff0c; 全站功能是通过ajax响应实现的&#xff0c;但本人技术有限&#xff0c;只会仿&#xff0c;不会移植&#xff0c;&#xff08;主要ajax这里不知道怎么弄&#xff09;。 另外就是网上…

【linux服务器篇】-Redis-RDM远程连接redis

redis desktop manager 使用远程连接工具RDM连接redis 市面上比较常见的其中一款工具redis desktop manager 简单的说&#xff1a; Redis Desktop Manager 简单的来讲就是Redis可视化工具&#xff0c;可以让我们看到Redis中存储的内容。 redis desktop manager是一款功能强…

大型综合医院、妇幼保健院智慧产科信息系统源码,支持二次开发,授权后可商用。

一套采用java语言开发&#xff0c;前端框架为Vue&#xff0c;ElementUIMySQL数据库&#xff0c;前后端分离架构的数字化产科管理系统源码&#xff0c;自主版权&#xff0c;多个大型综合医院、妇幼保健院应用案例&#xff0c;支持二次开发&#xff0c;授权后可商用。 系统特点&a…

Qt/QML学习-ListView

QML学习 ListView例程视频讲解代码 main.qml import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.15Window {id: windowwidth: 640height: 480visible: truetitle: qsTr("ListView")Rectangle {height: listView.heightwidth: listView.wi…

Pearson 相关系数的可视化辅助判断和怎么用

Pearson 相关系数的可视化辅助判断和怎么用 flyfish Pearson 相关系数 是一种用于衡量两个连续型变量之间线性相关程度的统计量。其定义为两个变量协方差与标准差的乘积的比值。公式如下&#xff1a; r ∑ ( x i − x ˉ ) ( y i − y ˉ ) ∑ ( x i − x ˉ ) 2 ∑ ( y i −…