volatile,synchronized,原子操作实现原理,缓存一致性协议

文章目录

  • 缓存一致性协议(MESI)
  • volatile
    • 1. volatile 的作用
    • 2.volatile的底层实现
    • 3,volatile 实现单例模式的双重锁(面手写)
  • synchronized
    • 1,基本用法
    • 2,可重入性
    • 3,Java对象头
    • 4,实现原理
      • (1)代码块同步的实现
      • (2)方法同步的实现
    • 5,锁的升级与对比
  • 原子操作的实现原理
    • 1,术语
    • 2,如何实现原子操作
    • 3,Java如何实现原子操作
        • CAS实现原子操作的三大问题

  • CAS及其损耗CPU性能

缓存一致性协议(MESI)

MESI 是四种缓存行状态的缩写:

状态英文全称说明
M (Modified)已修改缓存行已被当前CPU修改,与主存不一致,其他CPU不能持有该数据的有效副本
E (Exclusive)独占缓存行仅被当前CPU持有,与主存一致,其他CPU没有该数据的副本
S (Shared)共享缓存行被多个CPU共享,所有副本与主存一致
I (Invalid)无效缓存行数据已失效,必须从主存或其他CPU重新加载

MESI 的工作示例:

假设两个CPU核心(Core1和Core2)访问同一内存地址 X

  1. 初始状态
    • X 在主存中的值为 0
    • Core1和Core2的缓存中均无 X
  2. Core1 读取 X
    • Core1 缓存 X,状态变为 E (Exclusive)
    • 直接从主存加载 X=0
  3. Core2 读取 X
    • Core1 的 X 状态降级为 S (Shared)
    • Core2 也缓存 X,状态为 S
  4. Core1 修改 X=1
    • Core1 发送 总线事务,使 Core2 的 X 缓存行失效(状态变为 I
    • Core1 的 X 状态变为 M (Modified),并更新缓存值
  5. Core2 再次读取 X
    • 发现 X 缓存行无效(状态为 I
    • 向总线发送请求,Core1 将 X=1 写回主存,并降级为 S
    • Core2 重新加载 X=1,状态变为 S

volatile

volatile 是 Java 提供的一种轻量级同步机制,用于确保多线程环境下的 可见性禁止指令重排序,但它 不保证原子性

特性说明实现原理
可见性一个线程修改 volatile 变量后,其他线程立即可见新值内存屏障 + 缓存一致性协议(MESI)
有序性禁止 JVM 对 volatile 变量的读写操作进行重排序插入内存屏障指令
非原子性volatile 不能保证复合操作(如 i++)的原子性需配合 synchronized/CAS

1. volatile 的作用

(1) 保证可见性

  • 问题:普通变量在多线程环境下,一个线程修改后,其他线程可能无法立即看到最新值(由于 CPU 缓存)。
  • volatile 的解决方案
    • 写操作:立即刷新到主内存,并 使其他 CPU 缓存失效
    • 读操作:强制从主内存重新加载最新值。

(2) 禁止指令重排序

  • 问题:JVM 和 CPU 可能对指令进行优化重排,导致多线程环境下出现意外行为。
  • volatile 的解决方案
    • 通过 内存屏障(Memory Barrier) 禁止 JVM 和 CPU 对 volatile 变量的读写操作进行重排序。

2.volatile的底层实现

  1. 内存屏障

    • 写操作

      • **StoreStore 屏障:**位于volatile之前,确保 volatile 写之前的 所有普通写操作 都已完成(刷新到主内存)
      • **StoreLoad 屏障:**位于volatile之后,禁止当前 Store 与之后的 Load 重排序;强制刷新写缓冲区到主内存。
      // 线程1
      x = 1;                // 普通写
      StoreStoreBarrier();  // 确保 x=1 刷入内存
      volatileVar = 2;      // volatile 写
      StoreLoadBarrier();   // 确保 volatile 写对所有线程可见
      
    • 读操作

      • **LoadLoad 屏障:**位于volatile之后,防止 volatile 读与 后续的普通读操作 重排序
      • **LoadStore 屏障:**位于volatile之后,防止 volatile 读与 后续的普通写操作 重排序
      // 线程2
      int tmp = volatileVar; // volatile 读
      LoadLoadBarrier();    // 防止后续读重排序
      LoadStoreBarrier();   // 防止后续写重排序
      int b = x;            // 普通读(此时能看到线程1的 x=1)
      
  2. 缓存一致性协议

3,volatile 实现单例模式的双重锁(面手写)

双检锁/双重校验锁(DCL,即 double-checked locking)

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**是(即使用到这个变量时才会实例化)

**是否多线程安全:**是

**实现难度:**较复杂

**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

实例

public class Singleton {  private volatile static Singleton singleton;  private Singleton (){}  public static Singleton getSingleton() {  if (singleton == null) {  synchronized (Singleton.class) {  if (singleton == null) {  singleton = new Singleton();  }  }  }  return singleton;  }  
}
  • 私有构造器:禁止外部实例化
  • 双重检查
    • 第一次检查(无锁)
      避免每次调用 getSingleton() 都进入同步块,提升性能。
    • 第二次检查(加锁后)
      防止多个线程同时通过第一次检查后重复创建实例。
  • 同步锁(synchronized)
    • 保证 实例化过程的原子性,防止多线程并发创建多个实例。
  • volatile 关键字
    • 解决 指令重排序问题,确保其他线程不会获取到未初始化的对象。

如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排。

  • 为 Singleton对象分配内存
  • 将对象赋值给引用 singleton
  • 调用构造方法初始化成员变量

这种重排序会导致 singleton 引用在对象完全初始化之前就被其他线程访问到。具体来说,如果一个线程执行到步骤 2 并设置了 singleton 的引用,但尚未完成对象的初始化,这时另一个线程可能会看到一个“半初始化”的 Singleton对象。

  • 线程 A 执行到 if (singleton == null),判断为 true,进入同步块。
  • 线程 B 执行到 if (singleton == null),判断为 true,进入同步块。

如果线程 A 执行 singleton = new Penguin() 时发生指令重排序:

  • 线程 A 分配内存并设置引用,但尚未调用构造方法完成初始化。
  • 线程 B 此时判断 singleton != null,直接返回这个“半初始化”的对象。

这样就会导致线程 B 拿到一个不完整的 Penguin 对象,可能会出现空指针异常或者其他问题。

于是,我们可以为 singleton 变量添加 volatile 关键字,来禁止指令重排序,确保对象的初始化完成后再将其赋值给 singleton。

synchronized

1,基本用法

  • 加在静态方法上:锁定的是类

  • 加在非静态方法:锁定的是方法的调用者,当前实例。

  • 修饰代码块:锁定的是传入的对象

    并发学习之synchronized,JVM内存图,线程基础知识-CSDN博客

2,可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。

synchronized 就是可重入锁,因此一个线程调用 synchronized 方法的同时,在其方法体内部调用该对象另一个 synchronized 方法是允许的。

3,Java对象头

Java对象在内存中的布局分为三部分:对象头(Header)实例数据(Instance Data)和 对齐填充(Padding)。

对象头是synchronized实现的基础,它包含两部分信息:Mark Word(标记字段)和 Klass Pointer(类型指针,指向对象的类元数据的指针,JVM通过这个指针确定对象是哪个类的实例)。

Mark Word 的格式:

锁状态29 bit 或 61 bit1 bit 是否是偏向锁?2 bit 锁标志位
无锁001
偏向锁线程 ID101
轻量级锁指向栈中锁记录的指针此时这一位不用于标识偏向锁00
重量级锁指向互斥量(重量级锁)的指针此时这一位不用于标识偏向锁10
GC 标记此时这一位不用于标识偏向锁11

synchronized的同步是基于进入和退出Monitor对象实现的,每个Java对象都与一个Monitor相关联。

那什么是Monitor对象

在不同的锁状态下,Mark word会存储不同的信息,这也是为了节约内存常用的设计。当锁状态为重量级锁(锁标识位=10)时,Mark word中会记录指向Monitor对象的指针,这个Monitor对象也称为管程监视器锁

在这里插入图片描述

每个对象都存在着一个 Monitor对象与之关联。执行 monitorenter 指令就是线程试图去获取 Monitor 的所有权,抢到了就是成功获取锁了;执行 monitorexit 指令则是释放了Monitor的所有权。

4,实现原理

JVM规范中对于synchronized的实现分为两种方式:代码块同步和方法同步,它们底层采用了不同的实现策略,但最终都可以归结为对Monitor对象的操作。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

  1. 不是每个Java对象都有一个物理Monitor对象
    • 只有进入重量级锁状态时才会创建真正的Monitor对象
    • 偏向锁和轻量级锁阶段,锁信息存储在对象头中
  2. Monitor资源由JVM管理
    • Monitor对象不是Java层面的对象
    • 由JVM在需要时创建(通常位于C++层实现)

monitorenter 指令用于获取对象的监视器锁(Monitor lock),主要功能包括:

  1. 锁获取:尝试获取与对象关联的 Monitor
  2. 锁升级:根据竞争情况可能触发锁升级(偏向锁→轻量级锁→重量级锁)
  3. 重入计数:支持同一线程的锁重入
执行 monitorenter 时:
1. 检查对象头中的锁标志位- 如果是无锁状态(01):a. 尝试通过CAS将对象头Mark Word替换为当前线程指针(偏向锁)b. 成功则获取锁,失败则升级为轻量级锁- 如果是轻量级锁(00):a. 检查是否当前线程已持有锁(锁重入)b. 如果是,recursions+1c. 如果不是,自旋尝试获取或升级为重量级锁- 如果是重量级锁(10):a. 进入操作系统的互斥量等待队列
2. 获取成功后,对象头将记录锁状态和持有线程信息

monitorexit 指令用于释放对象的监视器锁,主要功能包括:

  1. 锁释放:释放对 Monitor 的持有
  2. 唤醒线程:在重量级锁状态下唤醒等待线程
  3. 重入处理:减少重入计数,只在完全释放时真正放开锁
执行 monitorexit 时:
1. 检查当前线程是否是锁的持有者- 如果不是,抛出 IllegalMonitorStateException
2. 减少重入计数(recursions)
3. 如果重入计数归零:a. 恢复对象头的无锁状态(轻量级锁)b. 或唤醒 EntryList 中的线程(重量级锁)
4. 如果是同步块结束处的 monitorexit:a. 正常退出同步区域
5. 如果是异常路径的 monitorexit:a. 仍然确保锁被释放b. 重新抛出异常

(1)代码块同步的实现

代码块同步是显式同步,通过monitorentermonitorexit指令实现:

  • 每个monitorenter必须有对应的monitorexit
  • 编译器会为同步块生成异常处理表,确保异常发生时也能释放锁
  • 可以针对任意对象进行同步

(2)方法同步的实现

方法同步是隐式同步,通过在方法访问标志中设置ACC_SYNCHRONIZED标志实现:

  • 调用方法时会隐式获取Monitor,没有显式的monitorentermonitorexit指令

  • 方法正常完成或异常抛出时会隐式释放Monitor

  • 同步的Monitor对象是方法所属的实例(非静态方法)或Class对象(静态方法)

  • JVM在方法调用时自动处理锁的获取和释放

特性monitorenter/monitorexitACC_SYNCHRONIZED
实现级别字节码指令方法访问标志
锁对象显式指定任意对象隐式使用 this 或 Class 对象
异常处理显式生成 monitorexitJVM 自动处理
可观察性可在字节码中直接看到只能通过访问标志识别
优化可能性较难优化更易被 JIT 优化

5,锁的升级与对比

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

在这里插入图片描述

  1. 偏向锁:

    • 设计目的:优化只有一个线程访问同步块的场景

    • 实现原理:HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

    • 升级触发条件

      • 另一个线程尝试获取该锁(产生竞争)
      • 调用 hashCode() 方法(因为偏向锁会占用哈希码位置)
  2. 轻量级锁:

    • 设计目的:优化线程交替执行同步块的场景最适合少量线程(建议≤2个活跃竞争线程)和短时间同步的场景

    • 轻量级锁加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,官方称为Displaced Mark Word。并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

    • 轻量级锁解锁:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

    • 升级触发条件

      • CAS 操作失败(表示有竞争|两个线程的CAS操作出现重叠|竞争发生在同一时间窗口)
      • 自旋获取锁超过一定次数
  3. 重量级锁

    依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。

原子操作的实现原理

1,术语

  • 缓存行:缓存行是CPU缓存的最小读写单位,通常为 64字节。三级缓存就是由缓存行组成。

    • L1/L2:每个核心独占,减少多核竞争。
    • L3:多核共享,避免频繁访问内存。
  • CAS:比较并且交换。CAS需要两个值,一个旧值,一个新值。旧值用来比较操作期间是否发生变化,如果没有发生变化才会交换新值。

  • CPU流水线技术

    时间轴  |  指令1   |  指令2   |  指令3   |  指令4   |
    --------+----------+----------+----------+----------+
    Cycle1  |   IF1    |          |          |          |
    Cycle2  |   ID1    |   IF2    |          |          |
    Cycle3  |   EX1    |   ID2    |   IF3    |          |
    Cycle4  |   MEM1   |   EX2    |   ID3    |   IF4    |
    Cycle5  |   WB1    |   MEM2   |   EX3    |   ID4    |
    Cycle6  |          |   WB2    |   MEM3   |   EX4    |
    Cycle7  |          |          |   WB3    |   MEM4   |
    Cycle8  |          |          |          |   WB4    |
    
  • 内存顺序冲突:内存顺序冲突 是由于 CPU/编译器优化导致的 指令重排问题导致的内存访问顺序与程序逻辑顺序不一致,从而引发数据竞争、逻辑错误等问题。

2,如何实现原子操作

  1. 总线锁
  2. 缓存锁:处理器标记该缓存行为 “锁定” 状态,阻止其他核心同时访问。

总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。

有两种情况不适用缓存锁:

  • 操作的数据没有缓存在缓存行中,或者操作数据跨了多个缓存行会使用总线锁
  • 某些处理器不支持

3,Java如何实现原子操作

  • **AtomicInteger**等原子类

  • 使用volatile,synchronized关键字

  • 使用CAS循环实现原子操作

        /** * 使用CAS实现线程安全计数器 */private void safeCount() {for (;;) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;}}}AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
    ref.compareAndSet(100, 101, stamp, stamp + 1);  // 检查值和版本号
    
CAS实现原子操作的三大问题
  • ABA问题:CAS在操作值时,如果一个值由A变为B又变为A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。解决思路是使用版本号,如上AtomicStampedReference
  • 循环时间长CPU开销大,自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作,如果是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。当多个线程同时竞争同一变量时,大量 CAS 操作会失败,导致线程自旋(循环重试)。自旋期间线程持续占用 CPU,执行无效循环,消耗 CPU 周期。

假设有1000线程并且这个CPU切换比较快速,其中一个CAS成功了,那剩余的999个就都白计算了,还不如加锁禁止其他线程操作,这样不会造成CPU的剧烈浪费。所以CAS只适合低烈度的并发。

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

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

相关文章

webfuture:如何屏蔽后台发文界面的保存为新文章按钮?

问题描述&#xff1a; 如何屏蔽后台发文界面的保存为新文章按钮&#xff1f; 问题解决&#xff1a;修改这个文件 /Admin/Content/Base/css/base.css 定义这个的id saveAsNewItemSubmit #saveAsNewItemSubmit{display: none;}

SpringBoot集成第三方jar的完整指南

原文地址&#xff1a;https://blog.csdn.net/weixin_43826336/article/details/141640152?ops_request_misc%257B%2522request%255Fid%2522%253A%25227d4118ef2d572ba4428caf83f1d2bb28%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id7d4118…

题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转

题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转 时间限制: 2s 内存限制: 192MB 提交: 1046 解决: 318 题目描述 小明创造了一个函数 f(x) 用来翻转 x 的二进制的数位&#xff08;无前导 0&#xff09;。比如f(11) 13&#xff0c;因为 11 (1011)2&#xff0c;将其左右翻转…

word为跨页表格新加表头和表名

问题&#xff1a; 当表格过长需要跨页时&#xff08;如下图所示&#xff09;&#xff0c;某些格式要求需要转页接排加续表。 方法一&#xff1a; 1、选中表格&#xff0c;在“表布局”区域点开“自动调整”&#xff0c;选择“固定列宽”&#xff08;防止后续拆分表格后表格变…

Ubuntu上进行VS Code的配置

1. 安装VS code sudo snap install code --classic 2. 安装GCC sudo apt install build-essential 3. 安装VS Code中文包 打开 VS Code 点击左侧活动栏中的扩展图标(或按Ctrl+Shift+X) 在搜索框中输入:Chinese (Simplified) 选择由 Microsoft 提供的 中文(简体)语言包…

vr中风--数据处理模型搭建与训练2

位置http://localhost:8888/notebooks/Untitled1-Copy1.ipynb # -*- coding: utf-8 -*- """ MUSED-I康复评估系统&#xff08;增强版&#xff09; 包含&#xff1a;多通道sEMG数据增强、混合模型架构、标准化处理 """ import numpy as np impor…

【LLM vs Agent】从语言模型到智能体,人工智能迈出的关键一步

目录 一、什么是 LLM&#xff1f;语言的天才&#xff0c;思维的起点 ✅ 特点小结&#xff1a; 二、什么是 Agent&#xff1f;智能的执行者&#xff0c;自主的决策者 ✅ 特点小结&#xff1a; 三、LLM 与 Agent 的关系&#xff1a;是工具&#xff0c;更是大脑 四、案例实战…

安装DockerDocker-Compose

Docker 1、换掉关键文件 vim /etc/yum.repos.d/CentOS-Base.repo ▽ [base] nameCentOS-$releasever - Base - Mirrors Aliyun baseurlhttp://mirrors.aliyun.com/centos/$releasever/os/$basearch/ gpgcheck1 enabled1 gpgkeyhttp://mirrors.aliyun.com/centos/RPM-GPG-KEY-C…

Perl One-liner 数据处理——基础语法篇【匠心】

Perl&#xff08;Practical Extraction and Report Language&#xff09;是一种功能强大且灵活的脚本语言&#xff0c;因其强大的文本处理能力和简洁的语法而广受开发者和系统管理员的喜爱。特别是在命令行环境下&#xff0c;Perl 的 one-liner&#xff08;单行脚本&#xff09…

Go语言defer关键字:延迟执行的精妙设计

深度解析Go语言defer关键字&#xff1a;延迟执行的精妙设计 引言 在Go语言中&#xff0c;defer语句是一种独特而强大的控制流机制&#xff0c;它通过​​延迟执行​​的方式解决资源管理、错误处理和异常恢复等关键问题。理解defer的工作原理是掌握Go并发编程和错误处理的关键…

C#项目07-二维数组的随机创建

实现需求 创建二维数组&#xff0c;数组的列和宽为随机&#xff0c;数组内的数也是随机 知识点 1、Random类 Public Random rd new Random(); int Num_Int rd.Next(1, 100);2、数组上下限。 //定义数组 int[] G_Array new int[1,2,3,4];//一维数组 int[,] G_Array_T …

.NET WinForm图像识别二维码/条形码并读取其中内容

需求:图像识别出一张图片中的二维码或者条形码&#xff0c;并读取其中内容。 一、安装库(特别注意&#xff0c;网上很多都没说清楚) 如果是基于.net framework&#xff0c;则安装ZXing.Net(建议0.14.0版本左右&#xff0c;具体看实际&#xff0c;版本太高&#xff0c;部分接口…

Guava限频器RateLimiter的使用示例

文章目录 1. 背景说明2. API与方法3. 示例代码3.1 基础工具方法3.2 测试任务类3.3 测试和统计方法3.4 测试两种模式的限频器3.5 测试缓冲时间与等待耗时 4. 完整的测试代码5. 简单小结 1. 背景说明 高并发应用场景有3大利器: 缓存、限流、熔断。 也有说4利器的: 缓存、限流、…

(面试)获取View宽高的几种方式

Android 中获取 View 宽高的几种方式&#xff0c;以及它们的适用场景和注意事项&#xff1a; 1. View.getWidth() 和 View.getHeight() 原理: 直接从 View 对象中获取已经计算好的宽度和高度。 优点: 简单直接。 缺点: 在 onCreate()、onStart() 等生命周期方法中&#xff0…

PostgreSQL pgrowlocks 扩展

PostgreSQL pgrowlocks 扩展 pgrowlocks 是 PostgreSQL 的一个系统扩展&#xff0c;用于显示表中行级锁定信息。这个扩展特别适合诊断锁争用问题和性能调优。 一、扩展安装与启用 1. 安装扩展 -- 使用超级用户安装 CREATE EXTENSION pgrowlocks;2. 验证安装 -- 查看扩展是…

JavaSE知识总结 ~个人笔记以及不断思考~持续更新

目录 字符串常量池 如果是创建对象还会吗&#xff1f; Integer也是在字串常量池中复用&#xff1f; 字符串拼接 为什么String是不可变的&#xff1f; String的不可变性是怎么做的&#xff1f; 外部代码不能创建对象&#xff1f; 构造方法不是私有的吗&#xff1f; 怎么…

使用HTTPS进行传输加密

文章目录 说明示例&#xff08;公网上的公开web&#xff09;安装SSL证书Certbot 的 Webroot 模式 和 Standalone 模式的区别**Webroot 模式****Standalone 模式** 技术对比表Node.js 场景下的最佳实践推荐方案&#xff1a;**Webroot 模式**Standalone 模式应急使用&#xff1a;…

驱动开发(2)|鲁班猫rk3568简单GPIO波形操控

上篇文章写了如何下载内核源码、编译源码的详细步骤&#xff0c;以及一个简单的官方demo编译&#xff0c;今天分享一下如何根据板子的引脚写自己控制GPIO进行高低电平反转。 想要控制GPIO之前要学会看自己的引脚分布图&#xff0c;我用的是鲁班猫RK3568&#xff0c;引脚分布图如…

ArcGIS Pro 3.4 二次开发 - 布局

环境:ArcGIS Pro SDK 3.4 + .NET 8 文章目录 布局1 布局工程项1.1 引用布局工程项及其关联的布局1.2 在新视图中打开布局工程项1.3 激活已打开的布局视图1.4 引用活动布局视图1.5 将 pagx 导入工程1.6 移除布局工程项1.7 创建并打开一个新的基本布局1.8 使用修改后的CIM创建新…

OpenCV 图像像素的算术操作

一、知识点 1、operator (1)、MatExpr operator (const Mat & a, const Mat & b); a、a和b的行数、列数、通道数得相同。 b、a和b的每个像素的每个通道值分别相加。 (2)、MatExpr operator (const Mat & a, const Scalar & s); a、若a…