Java内存模型

news2024/11/19 3:22:42

@TOC

Java内存模型

Java内存模型(Java Memory Model,JMM) 是《Java虚拟机规范》中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK5开始 JMM才正真成熟,完善起来。

Java内存模型的主要目的是定义程序中各种变量(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则

Java内存模型规定了所有的变量都存储在主内存(Main Memory)。每条线程都有自己的工作内存(Working Memory),用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程、主内存、工作内存三者的交互关系如下图。

在这里插入图片描述

内存交互

关于一个变量如何从工作内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可在分的(对 double 和 long 类型的变量来说,load、store、read 和 write操作在某些平台上允许有例外)。

  • lock(锁定):作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。
  • read(读取):作用域工作内存的变量,它把一个变量的值从工作内存传输到线程的工作内存中,以便随后的 load 使用。
  • load(载入):作用于工作内存中的变量,它把 read 操作从主内存中得到的变量值放到工作内存中的变量副本中。
  • use(使用):作用于工作内存中的变量,它把工作内存中的一个变量值传给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接受的值赋给工作内存中的变量,每当虚拟机遇到一个需要使用变量赋值的字节码指令时执行此操作。
  • store(赋值):作用于工作内存中的变量,它把工作内存中的一个变量传递到主内存中,以 write 操作使用。
  • write(赋值):作用于主内存中的变量,他把 store 操作传递来的值放入到主内存的变量中。

它们的使用规则如下:

  • 不允许 read 和 load、store 和 write 操作之一单独出现。
  • 不允许一个线程丢弃它最近的assign操作,即一个变量在工作内存中被修改后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程工作内存同步回主内存中。
  • 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化的(load 或 assign)的变量,即对一个变量实施 use 或 store 操作之前,必须先执行 load 或 assign操作。
  • 一个变量在同时个时刻治愈系一条线程对器进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次 lock 后,必须执行相同次数的 unlock 操作,变量才会被解锁。
  • 如果对一个变量执行 lock 操作,将会情况工作内存中的值,在执行引擎使用这个这个变量前,需要重新执行 load 或 assign 操作对 变量进行初始化。
  • 如果一个变量实现没有被 lock 操作锁定,那就不会允许对他执行 unlock 操作,也不允许去 unlock 一个被其它线程锁定的变量。
  • 对一个变量执行 unlock 操作前,必须先把此变量同步回主内存中(执行 store ,write 操作)。

JMM对volatile变量定义的特殊规则

Java内存模型中对volatile变量定义的特殊规则定义。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store 和 write 操作时需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作是 load 的时候,线程T才能对变量V执行use动作;并且,只有当线程 T 对变量 V 执行的后一个动作是 use 的时候,线程T才能对变量V执行load 动作。线程 T 对变量 V 的 load、read 动作相关联,必须连续且一起出现。

     这条规则要求在工作内存中,每次使用 V 前必须先从主内存刷新最新的值,用于保证能看见其它线程对变量V所作的修改。
    
  • 只有当线程 T 对变量 V 执行的前一个动作是 assign 的时候,线程 T 才能对变量V执行 store 动作;并且,只有当线程 T 对变量 V执行的后一个动作是 store 时候,线程T 才能对变量 V 执行 assign 动作。线程 T 对变量 V 的 assign 动作 可以认为是线程 T 对变量 V 的 store、write动作相关联的,必须连续且一起出现。

     这条规则要求在工作内存中,每次修改 V 后 都必须立刻同步回主内存中,用于保证其它线程可以看到子集对变量 V 所做的修改。
    
  • 假定动作A 是线程 T 对变量V实施的 use 或 assign 动作,假定动作 F 是和动作A相关联的 load 或 store 动作,假定动作 P 和动作 F 相应的对变 V 的 read 或 write 动作;与此类似,假定动作 B 是线程 T 对变量 W 实施的 use 或 assign 动作,假定动作 G 是和动作B 相关联的 load 或 store 动作,假定动作 Q是和动作G相应的对变量W的 read 或 write 动作。如果 A 先于B,那么 P先于Q。

     这条规则要求 volatile 修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。
    

先行发生原则(happens-before)

下面是Java内存模型下一些 "天然的" 先行发生关系。
  • 程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 监视器锁规则:在监视器上的解锁(unlock)操作先行发生于后面对同一个锁的加锁(lock)操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于对该volatile变量的读操作。
  • 线程启动规则:线程上对 Thread.start 的调用先行发生于该线程的每一个动作。
  • 线程结束规则:线程中的任何操作都必须在其它线程检测到该线程已结束之前执行,或者从Thread.join 中返回,或者在调用 Thread.isAlive 时返回false。
  • 中断规则:当一个线程在另一个线程上调用 Interrupt 时,必须在被中断线程检测到 interrupt 调用之前执行(通过抛出 InterruptException,或者调用 isInterrupted 和 interrupted)。
  • 终结器规则:一个对象的初始化完成(构造函数执行完成)发行发生于它的 finalize() 方法的开始。
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,则操作 A 一定先行发生于操作 C 。

在这里插入图片描述

在Java类库中提供的其它 Happens-Before 排序包括:

  • 将一个元素放入一个线程安全的容器的操作先行发生于从该容器中获取这个元素的操作。
  • 在 CountDownLatch 上的倒数操作将在线程从闭锁上的 await 方法返回之前执行。
  • 释放 Semaphore 许可的操作将在从该 Semaphore 上获取一个许可之前执行。
  • Future 表示的任务的所有操作将在从Future.get 中返回之前执行。
  • 向Executor提交一个Runnable 或 Callable 的操作将在任务开始之前执行。
  • 一个线程到达CyclicBarrier 或 Exchanger 的操作将在其他到达该栅栏或交换点的线程被释放之前执行。如果 CyclicBarrier 使用一个栅栏操作,那么到达栅栏将在栅栏操作之前执行,而栅栏操作又会在线程从栅栏中释放之前执行。

内存屏障

内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的一个同步点,使得此点之前的所有读写操作操作都执行后才可以开始执行此点之后的操作),避免代码重排序。代码屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令是插入特点的内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile 不能保证原子性。

内存屏障之前的所有写操作都要写回主内存

内存屏障之后的所有读操作都能获得所有内存屏障之前写操作的最新结果(实现了可见性)

写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行。

读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。

因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话:对一个volatile变量的写,先行发生于任意后续对这个volatile变量的读,也叫写后读。

分类

内存屏障粗分有两种,分别是 读屏障(Load Barrier) 和 写屏障(Store Barrier)

读屏障(Load Barrier):在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。

写屏障 (Store Barrier):在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。

再来看看源码 :首先打开 Unsafe.class ,发现都是本地方法
在这里插入图片描述
再打开其实现类:Unsafe.cpp
在这里插入图片描述
orderAccess.hpp
在这里插入图片描述
从上面的的源码可以看到,两种内存屏障又可以细分为四种,其作用如下:

屏障类型指令说明说明
LoadLoadLoad1; LoadLoad; Load2保证 Load1 的读取操作在 Load2 及其之后的读取操作之前执行
StoreStoreStore1; StoreStore; Store2在 Store2 及其后的写入操作执行之前,保证 Store1 的写入操作已刷新到主内存
LoadStoreLoad1;LoadStore; Store2在 Store2 及其后的写入操作执行之前,保证 Load1 的读取操作已读取结束
StoreLoadStore1; StoreLoad; Load2在 Load2 及其后的读取操作执行之前 ,保证Store1 的写入操作已刷新回主内存

最后 orderAccess_linux_x86.inline.hpp

inline void OrderAccess: : loadload()	{ acquire(); }
inline void OrderAccess: : storestore() { release(); }
inline void OrderAccess: : loadstore() 	{ acquire(); }
inline void OrderAccess: : storeload()	{ fence(); }

inline void OrderAccess::acquire() {
  volatile intptr_t local_dummy;    
#ifdef AMD64
  __asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) :: "memory");
#else
  __asm__ volatile ("movl 0(%%esp), %0" : "=r" (local_dummy) :: "memory");	
#endif // AMD64
}
                  
inline void OrderAccess::release() {
  // Avoid hitting the same cache-line from
  // different threads.
  volatile jint local_dummy = 0;
}

inline void OrderAccess::fence() {
  if (os::is_MP()){
	// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
	__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
	__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
	}
}

volatile 变量

关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制。

volatile 变量只能确保 可见性 和 有序性(指令禁重排),不能确保原子性。

volatile 变量通常用作某个操作完成,发生中断或者状态的标志。

当且仅当满足以下条件时,才应该使用 volatile 变量
- 对变量的写入操作不依赖变量的当前值或者可以保证只有单个线程更新变量的值。
- 改变量不会与其它状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁

可见性

可见性:一个线程修改了这个变量,其它线程都是可以立即得知的。

案例

public class VisibilityTest {
    // 可见性测试
    volatile static boolean flag = false ;
    public static void main(String[] args) {

        Thread.yield(); // 阻塞当前线程,直到 thread执行完成
        new Thread( ()->{
            flag = true; // 修改为真
        }).start();
        
        // 等待修改线程结束
        while (Thread.activeCount() > 1)
        	Thread.yeild();
        System.out.println("After changed");
        while (flag){
            // 空旋,如果 thread的修改 可见
            // 那么程序将一直 空旋
        }
    }
}

在这里插入图片描述
可以看到 thread 线程将 flag 修改为 true之后,main线程一直在运行,说明thread对volatile变量flag的修改,main线程可见。

原子性测试

原子性: 是指 一个操作是不可中断的,要么全部执行成功要么全部执行失败。

public class AtomicityTest {
    
    private  volatile static int num1 = 0;
    private  static int num2 = 0;

    public  static void increment1(){
    	// num++ 操作并不是一个原子性的操作
    	// num++ 是一个“读取-修改-写入”的操作序列,并且状态结果依赖于之前的状态
        num1++;
    }
    // 加锁的自增方法
    public  synchronized static void increment2(){
        num2++;
    }

    public static void main(String[] args) {
        // 启动 100个线程,每个线程对 num修改100次,理论上 num1 和 num2 都应该 为 10000
        for (int i = 0; i < 100 ; i++) {
            new Thread( ()->{
                for (int j = 0; j < 100; j++) {
                    increment1();
                    increment2();
                }
            }).start();
        }
        
        // 等待所有累加线程结束
        while (Thread.activeCount() > 1)
        	Thread.yeild();
        System.out.println("num1 = " + num1);
        System.out.println("num2 = " + num2);
    }
  }

在这里插入图片描述
发现 num1并不是 10000,说明 volatile 并不能保证变量的原子性

使用Javap反编译 increment1的代码,可以看到num++操作在Class文件中是由 4 条字节码构成的(return指令并不是由 num++产生的),从字节码层面就可很容易分析出并发失败的原因:当 getstatic 指令把 num的值取到操作数栈的栈顶时,volatile关键字保证了num的值是正确的,但是在执行 iconst_1,iadd指令的时候,其它线程可能将 num的值修改了,此时操作数栈顶的值就变成了过期的数据,所以 putstatic 指令执行过后就可能将较小的num值同步回主内存中
在这里插入图片描述
在这里插入图片描述

	由于 volatile变量只能保证可见性,当在不符合以下两条规则的运算场景中,仍然需要通过加锁来保证原子性
(synchronized、java.util.concurrent中的锁或者原子类)来保证原子性:
- 运算结果不依赖变量当前值,或者能够保证只有单一的线程对变量进行修改
- 变量不与其它状态变量一起纳入不变性条件中

有序性(指令重排序)

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序,并且重排后的指令绝对不能改变原有的串行语义!

1. 什么是指令重排序
禁止指令重排序是 volatile 变量的第二个作用,普通的变量只能保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与代码中的执行顺序一致。

int x = 1; 	①
int y = 2;	②
x = x + 1;	③ 
y = 3 * x;	④
// 我们想要的执行顺序是 ①②③④,但实际执行顺序可能是 ②①③④

重排序的分类和执行流程
在这里插入图片描述

重排序的分类和执行流程

编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序

指令级并行的重排序:处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺内存

系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。

数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

2. 指令重拍可能造成的影响
在这里插入图片描述
在这里插入图片描述

在单线程中,由于 线程内表现为串行,所以始终能确保最终执行结果和代码顺序的结果一致 ,所以不用担心指令重排会导致结果不正确;

下面三种情况,只要改变两个操作的执行顺序,程序的执行结果就会改变
在这里插入图片描述

案例

public class ReSortTest {

    volatile  static int num; // 使用volatile 禁止指令重排序

    volatile static boolean flag; // 保证可见性

    public  static  void set(){
        num = 6;
        flag = true;
    }

    public static void print(){
        while (flag) {
            num = num + 1;
            System.out.println("num = " + num);
        }
    }
}
//  如果允许指令重排序,就有可能出现,flag=true了(还没执行num = 6),就执行 num = num + 1;输出的就可能是 num = 1

volatile 实现原理

volatile 底层是通过内存屏障来实现有序性和可见性的

volatile读原理

在这里插入图片描述
在这里插入图片描述

volatile写原理
在这里插入图片描述

在这里插入图片描述

== 总结==
在这里插入图片描述

  1. volatile 读之后的 任何操作不能重排序到 volatile 读之前
  2. volatile 写之前的 任何操作 不能与 volatile 写重排序
  3. volatile 写不能 与 之后的 volatile 操作 重排序

部分内容摘抄至《深入理解Java虚拟机》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

不要再稀里糊涂的使用反射了,四万字带你搞定Java反射(JDK8)

文章目录前言Java反射体系概述类型信息&#xff08;Type&#xff09;GenericDeclarationAnnotatedType成员信息&#xff08;Member&#xff09;属性信息&#xff08;Field&#xff09;构造函数信息&#xff08;Constructor&#xff09;方法信息&#xff08;Method&#xff09;修…

毕业设计-基于机器视觉的指针式仪表智能识别

目录 前言 课题背景和意义 实现技术思路 实现效果图样例 前言 &#x1f4c5;大四是整个大学期间最忙碌的时光,一边要忙着备考或实习为毕业后面临的就业升学做准备,一边要为毕业设计耗费大量精力。近几年各个学校要求的毕设项目越来越难,有不少课题是研究生级别难度的,对本科…

C++ 复制构造函数

在讲解 C 的复制构造函数之前这里先明确一个概念&#xff0c;C 的复制构造函数的意思并不是字面上的意思复制一个构造函数&#xff0c;而是有一种专门用于复制内容的构造函数被叫做复制构造函数。 复制构造函数对于 C 来说是非常重要的概念&#xff0c;所以我们必须掌握并牢记…

jsp设备信息查询系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 jsp设备信息查询系统 是一套完善的web设计系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql&#xff0c;使用…

【算法笔记(六)】检索算法

算法笔记(六) 检索算法算法笔记(六)前言一、线性查找1.什么是线性查找2.需求规则3.人工图示演示4.代码实现二、二分查找1.什么是二分查找2.需求规则3.人工图示演示4.代码实现三.插值查找1.什么是插值查找2.需求规则3.人工图示演示4.代码实现四.斐波那契查找1.什么是斐波那契查找…

[附源码]计算机毕业设计基于springboot框架的资产管理系统设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

TCP三次握手四次挥手及常见问题解决方案

一、重要概念 位码即tcp标志位,有6种表示: SYN(synchronous建立连接) ACK(acknowledgement 表示响应、确认) PSH(push表示有DATA数据传输) FIN(finish关闭连接) RST(reset表示连接重置) URG(urgent紧急指针字段值有效) 二、三次握手四次挥手流程图 三、三次握手 第一次握手&am…

一段有关MPU配置代码的几个小疑问

当我们阅读一些STM32F7或STM32H7系列芯片例程&#xff0c;或者基于这两类芯片通过cubeMx进行配置并用到MPU功能时&#xff0c;往往会在代码里看到下面这段MPU配置。对这段代码可能有人有些疑问&#xff0c;这里重点一起聊聊其中的3个&#xff0c;供参考。第一个疑问&#xff0c…

【AI工程论文解读】04-通过Ease.ML/CI实现机器学习模型的持续集成(上)

作者&#xff1a;王磊 更多精彩分享&#xff0c;欢迎访问和关注&#xff1a;https://www.zhihu.com/people/wldandan 持续集成是一种软件开发实践&#xff0c;即团队开发成员经常集成他们的工作&#xff0c;通常每个成员每天至少集成一次&#xff0c;也就意味着每天可能会发生多…

机器人xacro设计+gazebo/rviz启动

机器人xacro设计gazebo/rviz启动项目需求方案机器人的本体设计机器人本体集成car_gazebo.xacroinertial惯性矩阵 head.xacro小车底盘base.xacro摄像头camera.xacro雷达laser.xacro机器joint关节控制器传感器sensor设计机器人joint关节控制器move.xacro摄像头传感器camera_senso…

Metabase学习教程:系统管理-7

使用MetabaseAPI MetabaseAPI简介。 本文介绍如何使用Metabase的API。我们自己使用该API连接前端和后端&#xff0c;因此您可以编写Metabase几乎可以执行的所有操作。 警告&#xff1a;MetabaseAPI可能会更改 开始之前有两个注意事项&#xff1a; API可能会更改。API与前端…

软件测试要学会哪些东西才能拿2w+的工资?

软件开发人员的月薪达到2万还是比较轻松的&#xff0c;但是软件测试人员想要月薪过万的话&#xff0c;我认为可以从两个方面去考虑&#xff1a; 1. 一种就是项目的测试负责人&#xff1a;测试人员需要对软件的整体性能改进提出建设性方案&#xff0c;所以很多软件测试人员最终…

数据结构和算法之《栈》详解

标题&#xff1a;栈的思路及代码实现 作者&#xff1a;Ggggggtm 寄语&#xff1a;与其忙着诉苦&#xff0c;不如低头赶路&#xff0c;奋路前行&#xff0c;终将遇到一番好风景 文章目录&#xff1a; 一、栈的概念及结构 1、1 栈的概念 1、2 栈的结构 二、栈的思路及代码实现详解…

电脑技巧:推荐5个非常实用的软件

❤️作者主页&#xff1a;IT技术分享社区 ❤️作者简介&#xff1a;大家好,我是IT技术分享社区的博主&#xff0c;从事C#、Java开发九年&#xff0c;对数据库、C#、Java、前端、运维、电脑技巧等经验丰富。 ❤️个人荣誉&#xff1a; 数据库领域优质创作者&#x1f3c6;&#x…

uniapp 之使用 u-upload 组件来实现图片上传

uniapp 之使用 u-upload 组件来实现图片上传前言一、官方示例用法分析二、关闭自动上传,使用手动上传的方式,代码html 代码js 代码css 代码总结分析前言 在使用 uniapp 开发的微信小程序中使用了图片上传功能,使用了 uniapp 的图片上传组件 注意&#xff1a;我这里后端接口接收…

小程序开发平台

小程序开发平台顾名思义就是一个可以开发小程序的地方。 小程序开发平台&#xff1a;【电脑浏览器输入3M.FKW.COM了解详情】 适合群体&#xff1a;企业、机构、个体户 小程序开发方式&#xff1a; 自建——可以通过套用小程序模板&#xff0c;利用拖拽式小程序开发工具&…

某组态软件工程文件加密机制探究

某组态软件工程文件加密机制探究 前言 在工业自动化控制领域&#xff0c;组态软件是数据采集与过程控制的专用软件&#xff0c;是实现人机交互必不可少的工具。工程设计人员使用组态软件在PC机上进行工程画面组态的编辑&#xff0c;然后把编译后的组态逻辑通过以太网或串口下载…

HSRP协议(思科私有)/VRRP协议(公有)

数据来源 1、HSRP热备份路由协议&#xff08;备份网关&#xff09; 出现背景&#xff1a; 如下图一个公司拉两条网线一条用来备份网关是192.168.0.253&#xff0c;平时用的网关是254&#xff0c;如果网关是254的这条网线出问题了就可以使用备份不影响公司让人员上网&#xff…

2022最新版 Java 学习线路图

第 1 阶段 - 企业级开发 - java 基础 学习掌握本阶段的内容&#xff0c;可以实现诸如迅雷下载软件、QQ 聊天客户端、中小网站&#xff0c;例如&#xff1a;小型旅游网站、小型电商网站的开发 第 2 阶段 - 企业及开发 - 基础框架 学习掌握本阶段内容&#xff0c;可以快速、规范的…

CRC校验——以SHT4xA温湿度传感器为例

CRC校验——以SHT4xA温湿度传感器为例一、简介二、计算方法&#xff08;一&#xff09;步骤&#xff08;二&#xff09;参考代码&#xff08;C语言&#xff09;&#xff08;三&#xff09;检验&#xff1a;CRC(0xBEEF) 0x92三、参考一、简介 循环冗余校验码&#xff08;CRC&am…