volatile解决有序性和可见性问题

news2024/9/24 17:14:11

线程可见性问题分析

什么是可见性?

如果一个线程对一个共享变量进行了修改 而其他线程不能及时地读取修改后的值 所以在多线程情况下该共享变量就会存在可见性问题

package com.alipay.alibabademo.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.FutureTask;

@Slf4j
public class ThreadDemo {


    public  static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int count =  0;
            while (!flag) {
                count++;
            }
        });
        thread.start();
        System.out.println("start Thread");
        Thread.sleep(1000);
        flag = true;
    }

}

通过flag变量判断是否终止循环 然而在main线程中通过修改flag变量的值破坏循环状态 但是实际情况是线程还一直处于运行状态 那么这是为什么呢?

上述案例问题出现的原因

在HotSpot虚拟机中内置了两个即时编译器 分别是ClientComplier(C1) 编译器和ServerComplier编译器(C2编译器)程序使用哪个编译器取决于JVM虚拟器的运行状态
ServerComplier是专门面向服务器端的 充分优化过的高级编译器他有一些比如代码消除等功能 上述例子中通过ServerComplier中的循环表达式外提进行优化变成了

package com.alipay.alibabademo.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.FutureTask;

@Slf4j
public class ThreadDemo {


    public  static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int count =  0;
            if (!flag){
                while (true) {
                    count++;
                }        
            }
        
        });
        thread.start();
        System.out.println("start Thread");
        Thread.sleep(1000);
        flag = true;
    }

}

被优化的代码对flag变量不具备变化的能力因此会导致当其他线程修改flag的值时该线程无法读取

volatile解决可见性问题

上述问题如果我们在共享变量上加一个volatile就可以充分的解决问题由此可见volatile可以禁止编译器的优化在多处理器环境下保证共享变量的可变性

package com.alipay.alibabademo.thread;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.FutureTask;

@Slf4j
public class ThreadDemo {


    public volatile static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int count =  0;
            while (!flag) {
                count++;
            }
        });
        thread.start();
        System.out.println("start Thread");
        Thread.sleep(1000);
        flag = true;
    }

}

可见性问题的本质

怎么提升CPU的利用率

CPU是计算机最核心的资源 当程序加载到内存后 操作系统会把当前线程分配给指定的CPU执行在获取CPU 的执行权后CPU从内存中取出指令进行解码并执行然后取出下一个指令继续执行
CPU在做运算的时候,无法避免要从内存中读取数据和指令 CPU的运算速度远远高于内存的I/O速度
CPU在做计算时必须与内存交互 即使是存储在磁盘上的数据也要先加载到内存中 CPU才能访问
请添加图片描述
基于上述的分析中可以看到 当CPU发起一个读操作时 在等待内存返回之前CPU都处于等待状态 直到返回之后CPU继续运行下一个指令 这个过程很显然会导致CPU资源的浪费为了解决这一问题 开发者在硬件设备 操作系统及编译器层面做了很多优化
1.在CPU层面增加了寄存器 来保存一些关键变量和临时数据还增加了CPU高速缓存以减少CPU和内存IO等待时间
2.在操作系统层面引入进程和线程 也就是在当前进程或线程处于阻塞状态时CPU会把自己的时间片分配给其他线程或进程使用 从而减少CPU空闲时间最大优化提升CPU使用率
3.在编译器层面面增加了指令优化 减少与内存的交互次数
这些优化虽然提升了CPU的利用率 但是也正好是导致可见性问题发生的原因

CPU高速缓存

CPU和内存的I/O操作时无法避免的 为了降低内存的I/O耗时 开发者在CPU中设计了高速缓存,用存储与内存交互的数据,CPU在做读操作时 ,会先从高速缓存中读取目标数据,如果目标数据不存在 就会从内存中加载目标数据并保存到高速缓存中在返回给处理器。
主流的X86处理器中 CPU高速缓存通常分为L1、L2、L3三级 具体的结构如下图
请添加图片描述
L1和L2是CPU的缓存属于CPU私有的
L1是CPU硬件的缓存 它分为数据缓存和指令缓存(指令缓存用来处理CPU必须要执行的操作信息 数据缓存用来存储CPU要操作的数据)
L2也是CPU硬件的缓存 容量比L1大 但是速度比L1慢
L3是CPU高速缓存中最大的一块同时也是访问速度最慢的缓存 也是共享的缓存
当CPU读取数据时 会先尝试从L1缓存中查找 如果L1缓存未命中 继续从L2和L3缓存中查找 如果在缓存行中没有命中到目标数据然后才会去访问内存。当从内存中加载数据时会优先扭转到L3缓存在到L2 最后到L1缓存 当后续再次访问存在于缓存行中的数据时CPU可以不需要访问内存 从而提升CPU效率

缓存行

CPU的高速缓存是由若干个缓存行组成的,缓存行是CPU高速缓存的最小存储单位 也是CPU和内存交换数据的最小单元在X86架构中 每个缓存行大小为64位 ,CPU每次从内存中加载8字节的数据作为一个缓存行保存到高速缓存中 这就意味着高速缓存中存在的是连续位置的数据

CPU缓存一致性问题

CPU高速缓存的设计大大的提升了CPU运算性能 但是它也存在一个问题,由于CPU中L1和L2缓存是CPU私有的 如果两个线程同时加载同一块数据并保存到高速缓存中 再分别进行修改 那么缓存一致性就得不到保证
请添加图片描述
如图所示 两个CPU的高速缓存都缓存了 count =0 的值 当CPU1将count+1 此时count=1 但是这个修改只对本地缓存可见 当后续CPU0在对Count进行计算的时候 它获取的值依然是0 此时缓存就不一致了

总线锁和缓存锁

为了解决CPU缓存一致性问题 在CPU层面引入了总线索和缓存锁机制

总线

所谓的总线就是与CPU交互 输入/输出设备传递消息的公共通道 当CPU访问内存进行数据交互时 必须经过总线来传输

总线锁

简单来说就是在总线上声明一个Lock#信号 这个信号能够确保共享内存只有当前CPU可以访问 其他处理器被阻塞 这样可以使得同一时刻访问共享内存 从而解决共享问题 但是代价就是性能的降低让人难以接受 所以后续引入了缓存锁

缓存锁

缓存锁指的是如果当前CPU访问的数据已经缓存在其他CPU的高速缓存中 那么CPU不会再总线上声明Lock#信号而是采用缓存一致性协议来保证多个CPU的缓存一致性
CPU最终采用那种锁解决缓存一致性问题 取决于当前CPU是否支持缓存锁 如果不支持就会采用总线锁 或者当前操作的数据不能被缓存在处理器内存 或者操作的数据跨多个缓存行时 也会使用总线锁

缓存一致性协议

MESI

缓存锁通过缓存一致性协议(MESI)来保证缓存的一致性

M(Modify)

表示共享数据只缓存在当前CPU缓存中 并且是被修改状态 缓存的数据和主内存的数据不一致

E(Exclusive)

表示缓存的独占状态 数据只缓存在当前CPU缓存中 并且没有被修改

S(Shared)

表示数据可能被多个CPU缓存 并且各个缓存中的数据和主内存数据一致

I(Invalid)

表示缓存已经失效

监听任务

MESI的四种状态会基于CPU对缓存行的操作而产生转移 所利益MESI针对不同的状态添加了不同的监听任务
1.如果一个缓存行处于M状态 则必须监听所有试图获取该缓存行对应的主内存地址的操作 如果监听这类操作的发生 则必须在该操作 之前把缓存行中的数据写回主内存

2.如果缓存行处于S状态 那么则必须要监听使该缓存行状态设置为Invalid 或者对缓存行执行Exclusive操作的请求 如果存在则必须要把当前缓存行状态设置为Invalid

3.如果一个缓存行状态为E状态 那么它必须要监听其他试图读取该缓存行对应的主内地址的操作一旦有这种操作该缓存行需要设置成Shared

整个的监听过程是通过CPU中Snoopy的嗅探协议完成的 该协议要求每个CPU缓存都可以监听到总线上的数据事件并作出相应的反应 所以CPU都会监听地址总线上的事件 当某个处理器发出请求时 其他CPU会监听到地址总线的请求根据当前缓存行的状态及监听的请求类型对缓存行状态进行更新

总结可见性问题的本质原因和解决方案

由于CPU高速缓存的设计导致了缓存一致性问题 又为了解决这一问题 设计了总线锁和缓存锁。

指令重排序

什么是指令重排序

指令重排序就是指编译器或CPU为了优化程序的执行性能而对指令进行重排序的一种手段 重排序会带来可见性问题。
从源码到最终运行的指令 会经过两个阶段的重排序
第一阶段:编译器重排序
在编译过程中编译器根据上下文分析对指令进行重排序 目的是减少CPU和内存的交互 重排序之后尽可能保证CPU从寄存器或缓存行中读取数据
第二阶段:处理器重排序 处理器重排序又分为两个部分
1.并行指令集重排序
处理器可以改变指令的执行顺序
2.内存系统重排序
引入Store Buffer缓冲区延时写入产生的指令顺序执行不一致的问题

as-if-serial语义

所有的程序指令都可以因为优化而被重排序 但是必须是在单线程环境下单线程环境下 运行结果和重排序之后的结果一致

指令重排序的本质

CPU通过引入了高速缓存来提升利用率 并且基于缓存一致性来保证缓存的一致性 但是缓存一致性会影响CPU的使用效率
假设存在一个S状态的缓存行(CPU0和CPU1共享同一个缓存行 )如果CPU9对缓存进行修改 那么CPU需要发送一个Invalidate消息到CPU1 在等待CPU1回复的这个时间段 CPU0一直处于空闲状态
在这里插入图片描述

Store Buffer

为了防止这种空闲等待状态 CPU层面又设计了一个Store Buffer

在这里插入图片描述
Cpu0引入Store Buffer的设计后 CPU0会先发送一个Invalidate消息给其他包含该缓存行的CPU1 并把当前修改的数据写入StoreBuffers中 然后继续执行后续的指令 等收到CPU1的Ack消息后 Cpu0再把Store Buffer挪到缓存行

Store Buffer引发的问题可见性问题

在这里插入图片描述
1.假设a 变量的缓存状态是SharedCpu0执行a=1的指令 此时a不存在cpu0的缓存中 但是在其他cpu缓存中他是Shared状态 所以cpu0会发送一个MESI协议消息 read invalidate给CPU1 企图从其他缓存了该变量 cpu中去读取值 并使得其他cpu缓存行失效
2.cpu0把a=1写入cpu0的store buffer中
3.CPU1收到read invalidate消息后 返回 ReadResponse 把 a=0返回 并让cpu1的缓存行失效
4.由于StoreBuffer存在 cpu0在等待cpu1返回之前就继续往下执行b=a+1的指令此时Cache 中还没有加载b 所以b=0
5.cpu0 收到其他cpu返回的结果 更新了缓存行 a=0 接着加载出了a=0
6.接着cpu0把store buffer a=1的数据同步到缓存行中
7.结果判断失败

Store Forwarding

为了解决Store Buffer引起的问题又设计了Store Forwarding 在每个CPU加载数据之前会先引用当前CPU的Store Buffers也就是说支持将CPU存入Store Buffers的数据传递给后续的加载操作而不需要经过Cache
在这里插入图片描述

Store Forwarding带来的指令重排序问题

在这里插入图片描述
1.CPU0执行a=1的指令 a是独占且a不存在cou0的缓存行中 因此cpu0把a=1写入StoreBuffer中并发送MESI协议消息给 cpu1
2.cpu1执行 b=1的操作 cpu1的缓存行中没有b的缓存 所以cpu1发出一个MESI协议消息 给cpu0
3.cpu0执行 b=1的指令 而B变量 存在于CPU0的缓存行中 也就说缓存行属于M或者E状态 因此直接把b=1写入缓存行
4.此时cpu0收到 cpu1发来的消息 将缓存行中的B=1返回给cpu1 并修改缓存状态为 S
5.CPU1 修改缓存行 b=1并将状态设置为S
6.获取b=1后 cpu1继续执行assert(a=1)的指令 此时cpu1的缓存行中 a=0
7.cpu1收到cpu0的消息把包含a=0的缓存行 返回给cpu0 并设置成I(失效)但此时这个过程比前面的异步步骤执行晚已经导致了问题
8.CPU0收到包含a的缓存行后 把 stre buffer中a=1同步到缓存行

Invalidate Queues(用于让缓存行失效的消息)

Store Buffes 确实进一步提升了CPU的利用率 但是Store Buffes本身的存储容量是有限的 在当前CPU的所有写入操作都存在缓存未命中的情况时 就会导致StoreBuffers 很容易被填满 被填满之后必须等待CPU返回 Invalidate Acknowledge消息 Store Buffers中对应的指令才能被清理 而这个过程CPU必须等待 无论该CPU中后续指令是否存在缓存未命中的情况
当前CPU 之所以要等待 Invalidate Acknowledge的消息才会去清理指令 是因为CPU必须要确保缓存的一致性 但是如果收到 Invalidate 消息的CPU当时处于繁忙状态 那么会导致 Invalidate Acknowledge消息返回延迟 ,我们发现CPU在发送 Invalidate Acknowledge消息之前 并不需要立刻使缓存行失效 反过来 我们也可以按照Store Buffes的设计理念增加一个 Invalidate Queues用来存放让缓存行失效的消息 也就是说 CPU收到Invalidate 消息时 把让该缓存行失效的消息放入 Invalidate Queues然后同步返回一个 Invalidate Acknowledge消息这样就大大的缩短了响应时间
在这里插入图片描述

Invalidate Queues导致的问题

增加了Invalidate Queues之后 CPU发出的Invalidate 消息能够很快得到其他CPU发送的 Invalidate Acknowledge消息从而加快了 Store Buffers中指令的处理效率 减少了CPU的阻塞问题 但是 Invalidate Queues存在会导致CPU内存系统wreite操作的重排序问题
在这里插入图片描述

  1. cpu0执行a=1,由于其有包含a的cache line,将a写入store buffer,并发出Invalidate a消息。
  2. cpu1执行while(b == 0),它没有b的cache,发出Read b消息。
  3. cpu1收到cpu0的Invalidate a消息,将其放入Invalidate Queue,返回Invalidate ACK。
  4. cpu0收到Invalidate ACK,将store buffer中的a=1刷新到cache line,标记为Modified。
  5. cpu0看到smp_wmb()内存屏障,但是由于其store buffer为空,因此它可以直接跳过该语句。
  6. cpu0执行b=1,由于其cache独占b,因此直接执行写入,cache line标记为Modified。
  7. cpu0收到cpu1发的Read b消息,将包含b的cache line写回内存并返回该cache line,本地的cache line标记为Shared。
  8. cpu1收到包含b(当前值1)的cache line,结束while循环。
  9. cpu1执行assert(a == 1),由于其本地有包含a旧值的cache line,读到a初始值0,断言失败。
  10. cpu1这时才处理Invalid Queue中的消息,将包含a旧值的cache line置为Invalid

内存屏障解决指令重排序问题

在不同的应用中为了防止CPU的指令重排序 必然会使用CPU提供的内存屏障指令 在Linux的内核中 这三种指令分别封装成smp_mb()、smp_rmb()、smp_wmb
smp_mb:
是全屏障 基于Lock指令来实现 ,声明lock指令后在多处理器环境下 通过总线锁/缓存锁机制来保证执行指令的原子指令
lock指令隐含了一个内存屏障的语义 也就是说 修饰了lock指令的数据能够避免CPU内存重排序文图
smp_wmb:
通过barrier方法实现写屏障

1.读屏障(ifence)

将Invalidate Queues中的指令立即处理 并且强制读取cpu的缓存行 执行 ifence指令之后的读操作不会被重排序到ifence指令之前这意味着其他cpu 暴露出来的缓存行对当前cpu可见

2.写屏障(sfence)

会把 store Buffers中修改刷新到本地缓存中 使得其他cpu可以看到这些修改 而且在执行sfence指令之后的写操作不会重排序到 sfence指令之前 这意味着sfence指令之前的写操作全局可见

3.读写屏障(mfence)

保证了 mfence指令执行前后的读写操作的顺序 同时要求执行 mfence指令之后的写操作全局可见 之前的写操作全局可见

Java Memory Mode

在多线程环境中导致可见性问题的根本原因是CPU的高速缓存及指令重排序 虽然CPU层面提供了内存屏障及锁的机制来保证有序性 然而在不同的CPU类型中 又存在不同的内存屏障指令 Java作为一个跨平台语言 必须要针对不同的底层操作系统和硬件提供统一的线程安全性保证 Java Memory Mode就是这样的一个模型

Java Memory Mode也是一种规范 该规范定义了线程和主内存之间访问规则的抽象关系

总结volatile怎么解决的可见性问题和指令重排序问题

volatile关键字会在JVM层面声明一个C++的volatile 他能防止JIT层面的指令重排序
在对修饰了volatile关键字的字段复制后 JVM会调用 storeload()内存屏障方法该方法声明了lock指令 该指令有两个作用
1.在CPU层面给stip赋值的指令会先存储到StoreBuffers中 所以Lock指令会使得StoreBuffers的数据刷新到缓存行
2.使得其他CPU缓存了该字段的缓存行失效 也就是让存储在Invalidate Queues中的对该字段缓存行失效指令立即生效,当其他线程再去读取该字段的值时会先从内存中或者其他缓存了该字段的缓存行中重新读取从而获得最新的值

Happens-Before模型

Happens-Before用来描述两个操作指令的顺序关系 如果一个操作和另外一个操作存在Happens-Before关系 那么意味着一个操作对第二个操作可见

Happens-Before规则

程序顺序规则(as-if-serial)

一个线程中 存在两个操作 x和y 并且x源代码执行在y之前

传递性规则

如果
A Happens-Before B
B Happens-Before C
那么A 必然 Happens-Before C 不管在单线程还是多线程中传递性规则都能够提供可见性保障

volatile规则

通过内存屏障来保证 volatile变量修饰的写操作一定 Happens-Before读操作

监视器锁规则

一个线程释放锁必须 Happens-Before 后续线程的加锁操作

start 规则

一个线程调用start方法之前的所有操作 Happens-Before 线程B的任意操作

join 规则

main线程执行了一个线程A的join方法并成功返回 那么线程A中的任意操作 Happens-Before 于main线程线程join方法返回之后的操作

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

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

相关文章

改进遗传算法在TSP问题中的应用(Matlab代码实现)

👨‍🎓个人主页:研学社的博客 💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜…

玩转Python图片处理(OpenCV)

OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,…

Qt扫盲-QLabel使用总结

QLabel使用总结一、QLabel 显示内容二、注意事项1. 格式注意2. 不保留先前状态3. 格式对齐4. 伙伴焦点三、信号和槽函数使用1. 信号2. 槽函数总结:QLabel 其实就是一个用来只读显示的简易控件。适合数据量很小的内容显示。QLabel用于显示 文本或图像。 不提供用户交…

深度学习之目标检测通用技巧

深度学习之目标检测通用技巧深度学习之目标检测通用技巧深度学习之目标检测通用技巧 一 数据增强 1.离线增强 离线增强:对数据集进行处理,数据的数目会变成增强因子原数据集的数目 2.在线增强 对输入模型的batch的数据进行增强,如旋转、平移、翻折等…

今天给大家介绍一篇基于SSM的教材管理系統的设计与实现

项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…

【Tensorflow学习三】神经网络搭建八股“六步法”编写手写数字识别训练模型

神经网络搭建八股“六步法”编写手写数字识别训练模型Sequential用法model.compile(optimizer优化器,loss损失函数,metrics["准确率"])model.fitmodel.summery六步法搭建鸢尾花分类网络class搭建具有非顺序网络结构MNIST数据集Fashion MNIST数据集用Tensorflow API:t…

java计算机毕业设计基于安卓Android的掌上酒店预订APP

项目介绍 网络的广泛应用给生活带来了十分的便利。所以把掌上酒店预订与现在网络相结合,利用java技术建设掌上酒店预订APP,实现掌上酒店预订的信息化。则对于进一步提高掌上酒店预订发展,丰富掌上酒店预订经验能起到不少的促进作用。 掌上酒店预订APP能够通过互联网得到广泛的…

基于风能转换系统的非线性优化跟踪控制(Matlab代码实现)

💥💥💥💞💞💞欢迎来到本博客❤️❤️❤️💥💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清…

一、CDD在诊断开发中的作用

本专栏将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希望能对大家有所帮助,与大家共同成长,早日成为一名车载诊断、通信全栈工程师。 本文介绍CDD在诊断开发中的作用,欢迎各位朋友订阅、评论,可以提…

如何评价模型的好坏?

回归: MSE(均方误差)—— 判定方法:值越小越好(真实值-预测值,平方之后求和平均)RMSE(均根方误差)—— 判定方法:值越小越好(MSE开根号&#xff…

Dijkstra最短路径算法

参考:(3条消息) Dijkstra算法图文详解_一叶执念的博客-CSDN博客_迪杰斯特拉算法 如图,假设图中共有n条路径(如D-C-E),根据路径长度进行小到大排序。 1、起点到达某终点的距离是无穷符号,表示该起点还需要借…

27岁到来之际,我在阿里实现了年薪40W+的小目标

顺着大佬的思路,我分析了自己的实际水平和状况: 1、技术不精不成体系:技术能力浮于表面,对底层逻辑和架构不了解,也不知道如何系统化进行学习; 2、遇到职场瓶颈期:站在3年职场的分水岭上,没有…

Linux网络原理及编程(8)——第十八节 数据链路层

目录 1、MAC地址 2、MAC帧 3、MAC帧协议 4、MTU 5、ARP请求和应答 各位好,博主新建了个公众号《自学编程村》,拉到底部即可看到,有情趣可以关注看看哈哈,关注后还可以加博主wx呦~~~(公众号拉到底部就能看到呦~~&a…

基于java+springmvc+mybatis+jsp+mysql的新冠肺炎疫苗接种管理系统

项目介绍 新冠疫苗接种管理系统,在网站首页可以查看首页,疫苗信息,疫苗资讯 ,个人中心,后台管理,在线客服等内容,并进行详细操作。管理员登录进入系统可以查看首页,个人中心&#x…

vue自定义keepalive组件的问题解析

前一阵来了一个新的需求,要在vue项目中实现一个多开tab页面的功能,本来心想,这不简单嘛就是一个增加按钮重定向吗?(当然如果这么简单我就不写这个文章了)。很快写完,提交测试。测试大哥很快就提…

一份奇奇怪怪的地图设计书

地图设计书 地图设计是通过研究实验制定新编地图的内容、表现形式及其生产工艺程序的工作,是地图制图学各种活动的中心,贯穿整个地图制图过程。本设计选择了福建省龙岩市作为研究区域,并结合相应区域的土地利用类型、水系、道路等数据&#…

儿童剧本杀行业是好生意吗?剧本杀门店管理系统

红楼梦、西游记、水浒传、三国演义是中国四大名著,几乎每个中国人上到70岁老人,下到十岁小学生都知道,同时还有花木兰、包青天、八仙过海等故事也都耳濡目染,小说描述的淋漓尽致,影视剧老戏骨们将每个角色刻画的深入人…

spring-aop源码分析(3)完结_执行流程分析

本文详细介绍Spring AOP的执行阶段流程。 Cglib代理的代理拦截逻辑在DynamicAdvisedInterceptor中,JDK代理的拦截逻辑在JdkDynamicAopProxy中,本文将从这两个类入手分析Spring AOP的执行阶段流程。 DynamicAdvisedInterceptor private static class D…

Modbus数据采集方案

目录 目标 Modbus协议简介 配置界面以及实例 概述 modbus协议应该是工业行业应用最广泛的协议,由于其协议简单、通讯标准、扩展性强的特点,被各个行业大量的应用。作为通讯网关机来说,设计一个便捷易懂的配置方式显得尤其重要。本方案基于…

多模态中的指令控制(InstructPix2Pix,SayCan)

InstructPix2Pix: Learning to Follow Image Editing Instructions 图像的语言指令生成。目的是遵循人工指令去编辑图像,即给定输入图像和一个如何编辑它的文本指令,模型尝试遵循这些指令来编辑图像。 这份论文与现有基于文本的图像编辑工作们最大的不同…