Java面试--CAS

news2025/1/23 10:32:41

这里写目录标题

  • 一、概念
  • 二、CAS 如何保证原子性
    • 2.1、总线锁定
    • 2.2、缓存锁定
  • 二、底层原理
  • 三、CAS典型应用
  • 四、CAS问题
    • 4.1、循环时间长,开销很大
    • 4.2、只能保证一个共享变量的原子操作
    • 4.3、引出来 ABA 问题

一、概念

判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。映射到操作系统就是一条 cmpxchg 硬件汇编指令(保证原子性),其作用是让 CPU 将内存值更新为新值,但是有个条件,内存值必须与期望值相同,并且 CAS 操作无需用户态与内核态切换,直接在用户态对内存进行读写操作(意味着不会阻塞/线程上下文切换)

假如说有 3 个线程并发的要修改一个 AtomicInteger 的值,底层机制如下:

  1. 每个线程都会先获取当前的值,接着走一个原子的 CAS 操作。原子的意思就是这个 CAS 操作一定是自己完整执行完的,不会被别人打断;
  2. 然后 CAS 操作里,会比较一下,现在的值是不是刚才获取到的那个值。如果是,说明没人改过这个值,然后设置成累加 1 之后的一个值;
  3. 如果有人在执行 CAS 的时候,发现之前获取的值跟当前的值不一样,会导致 CAS 失败。失败之后,进入一个无限循环,再次获取值,接着执行 CAS 操作;

二、CAS 如何保证原子性

原子性是指一个或者多个操作在 CPU 执行的过程中不被中断的特性,要么执行,要不执行,不能执行到一半(不可被中断的一个或一系列操作)

为了保证 CAS 的原子性,CPU 提供了下面两种方式

  • 总线锁定
  • 缓存锁定

在多处理器环境下,LOCK# 信号可以确保处理器独占使用某些共享内存。LOCK 可以被添加在下面的指令前:

ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG

通过在 inc 指令前添加 LOCK 前缀,即可让该指令具备原子性。多个核心同时执行同一条 inc 指令时,会以串行的方式进行,也就避免了上面所说的那种情况。那么这里还有一个问题,LOCK 前缀是怎样保证核心独占某片内存区域的呢?答案如下:

在 Intel 处理器中,有两种方式保证处理器的某个核心独占某片内存区域。第一种方式是通过锁定总线,让某个核心独占使用总线,但这样代价太大。总线被锁定后,其他核心就不能访问内存了,可能会导致其他核心短时内停止工作。第二种方式是锁定缓存,若某处内存数据被缓存在处理器缓存中。处理器发出的 LOCK# 信号不会锁定总线,而是锁定缓存行对应的内存区域。其他处理器在这片内存区域锁定期间,无法对这片内存区域进行相关操作。相对于锁定总线,锁定缓存的代价明显比较小

2.1、总线锁定

总线(BUS)是计算机组件间的传输数据方式,也就是说 CPU 与其他组件连接传输数据,就是靠总线完成的,比如 CPU 对内存的读写

总线锁定是指 CPU 使用了总线锁,所谓总线锁就是使用 CPU 提供的 LOCK# 信号,当 CPU 在总线上输出 LOCK# 信号时,其他 CPU 的总线请求将被阻塞

在这里插入图片描述

2.2、缓存锁定

总线锁定方式虽然保证了原子性,但是在锁定期间,会导致大量阻塞,增加系统的性能开销,所以现代 CPU 为了提升性能,通过锁定范围缩小的思想设计出了缓存行锁定(缓存行是 CPU 高速缓存存储的最小单位)。

所谓缓存锁定是指 CPU 对缓存行进行锁定,当缓存行中的共享变量回写到内存时,其他 CPU 会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据,缓存锁定是基于缓存一致性机制来实现的,因为缓存一致性机制会阻止两个以上 CPU 同时修改同一个共享变量(现代 CPU 基本都支持和使用缓存锁定机制)

二、底层原理

整体实现思路: 自旋(循环) + CAS算法

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

Unsafe 里的 CAS 操作相关

CAS 是一些 CPU 直接支持的指令,也就是我们前面分析的无锁操作,在 Java中 无锁操作 CAS 基于以下 3 个方法实现

// 第一个参数 o 为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
// expected 表示期望值,x 表示要设置的值,下面 3 个方法都通过 CAS 原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
 
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
 
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

并发包中的原子操作类(Atomic系列)

CAS 在 Java 中的应用,即并发包中的原子操作类(Atomic 系列),从JDK 1.5开始提供了 java.util.concurrent.atomic 包,在该包中提供了许多基于 CAS 实现的原子操作类,用法方便,性能高效,主要分以下 4 种类型

原子更新基本类型主要包括 3 个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

原子更新引用数据类型主要包括:

  • AtomicReference 原子操作引用对象类型

基本数据类型的原子操作类的实现原理和使用方式几乎是一样的,以 AtomicInteger 为例进行分析,AtomicInteger 主要是针对 int 类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,源码如下

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 获取指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();

	// 下述变量 value 在 AtomicInteger 实例对象内的内存偏移量
    private static final long valueOffset;

    static {
        try {
        	// 通过 unsafe 类的 objectFieldOffset() 方法,获取 value 变量在对象内存中的偏移
        	// 通过该偏移量 valueOffset,unsafe类的内部方法可以获取到变量 value 对其进行取值或赋值操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 当前 AtomicInteger 封装的 int 变量 value
    private volatile int value;

	public AtomicInteger() {
    }
    
    // 获取当前最新值
    public final int get() {
        return value;
    }
    
    // 设置当前值,具备 volatile 效果,方法用 final 修饰是为了更进一步的保证线程安全。
    public final void set(int newValue) {
        value = newValue;
    }
    // 最终会设置成 newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
    
    // 设置新值并获取旧值,底层调用的是 CAS 操作即 unsafe.compareAndSwapInt() 方法
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    
    // 如果当前值为 expect,则设置为update(当前值指的是value变量)
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

	/*
	    this:当前对象
	    valueOffset:内存偏移量,根据内存偏移地址可以获取数据
	*/
	public final int getAndIncrement() {
	    return unsafe.getAndAddInt(this, valueOffset, 1);
	}

    // 当前值减1,返回旧值,底层CAS操作
    public final int getAndDecrement() {
       return unsafe.getAndAddInt(this, valueOffset, -1);
    }
}

可以发现 AtomicInteger 原子类的内部几乎是基于前面分析过 Unsafe 类中的 CAS 相关操作的方法实现的,这也同时证明 AtomicInteger 是基于无锁实现的

我们发现 AtomicInteger 类中所有自增或自减的方法都间接调用 Unsafe 类中的 getAndAddInt() 方法实现了 CAS 操作,从而保证了线程安全,关于 getAndAddInt 其实前面已分析过,它是 Unsafe 类中 1.8 新增的方法,源码如下

public final int getAndAddInt(Object var1, long var2, int var4) {
   int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

可看出 getAndAddInt 通过一个 while 循环不断的重试更新要设置的值,直到成功为止,调用的是 Unsafe 类中的 compareAndSwapInt 方法,是一个 CAS 操作方法。这里需要注意的是,上述源码分析是基于JDK1.8 的,如果是 1.8 之前的方法,AtomicInteger 源码实现有所不同,是基于 for 死循环的,如下:

 
// JDK 1.7的源码,由 for 的死循环实现,并且直接在 AtomicInteger 实现该方法,
// JDK1.8 后,该方法实现已移动到 Unsafe 类中,直接调用 getAndAddInt 方法即可
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

AtomicReference 类实现自旋锁

自旋锁是假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用 CPU 的时间变长会导致性能急剧下降,因此 Java 虚拟机内部一般对于自旋锁有一定的次数限制,可能是 50 或者 100 次循环后就放弃,直接挂起线程,让出 CPU 资源

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

//使用 CAS 原子操作作为底层实现
public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t invoked myUnLock()");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(()-> {
            try {
                spinLockDemo.myLock();
                TimeUnit.SECONDS.sleep(5);
                spinLockDemo.myUnLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "AA").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()-> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        }, "BB").start();
    }
}
/*
    AA	 come in
    BB	 come in
    AA	 invoked myUnLock()
    BB	 invoked myUnLock()
*/

原子类更新引用

class User {
    User(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    private String name;
    private int age;
}

public class Solution {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) throws InterruptedException {
        AtomicReference<User> atomicReference = new AtomicReference<>();

        User user = new User("monster", 18);
        User updateUser = new User("jack", 25);

        atomicReference.set(user);
        System.out.println(atomicReference.get());
        atomicReference.compareAndSet(user, updateUser);
        System.out.println(atomicReference.get());
    }
}

/*
	User@74a14482
	User@1540e19d
*/

三、CAS典型应用

java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的,比如 AtomicInteger、AtomicLong。一般在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多;

在较多的场景都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题

public class Increment {
    private int count = 0;
    public void add() {
        count++;
    }
}

在并发环境下对 count 进行自增运算是不安全的,因为 count++ 不是原子操作,而是三个原子操作的组合:

  • 读取内存中的 count 值赋值给局部变量 temp;
  • 执行 temp + 1 操作;
  • 将 temp 赋值给 count;

所以如果两个线程同时执行 count++ 的话,不能保证线程一按顺序执行完上述三步后线程二才开始执行:

解决方案

1、Synchronized 加锁。同一时间只有一个线程能加锁,其他线程需要等待锁,这样就不会出现 count 计数不准确的问题了

public class Increment {
    private int count = 0;
    public synchronized void add() {
        count++;
    }
}

但是引入 Synchronized 会造成多个线程排队的问题,相当于让各个线程串行化了,一个接一个的排队、加锁、处理数据、释放锁,下一个再进来。同一时间只有一个线程执行,这样的锁有点重量级了

这类似于悲观锁的实现,需要获取这个资源,就给它加锁,别的线程都无法访问该资源,直到操作完后释放对该资源的锁。虽然随着 Java 版本更新,也对 Synchronized 做了很多优化,但是处理这种简单的累加操作,仍然显得"太重了"

2、Atomic 原子类。对于 count++ 的操作,完全可以换一种做法,Java 并发包下面提供了一系列的 Atomic 原子类,比如说 AtomicInteger

public static void main(String[] args) {
    public static AtomicInteger count = new AtomicInteger(0);
    public static void increase() {
        count.incrementAndGet();
    }
}

多个线程可以并发的执行 AtomicInteger 的 incrementAndGet(),意思就是把 count 的值累加 1,接着返回累加后最新的值。实际上,Atomic 原子类底层用的不是传统意义的锁机制,而是无锁化的 CAS 机制,通过 CAS 机制保证多线程修改一个数值的安全性

四、CAS问题

4.1、循环时间长,开销很大

自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率

4.2、只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij = 2a,然后用 CAS 来操作 ij。从 Java1.5 开始JDK提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作

4.3、引出来 ABA 问题

因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A -> B -> A 就会变成 1A -> 2B -> 3A

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

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

相关文章

Bootstrap5 表单浮动标签

默认情况下&#xff0c;标签内容一般显示在 input 输入框的上方&#xff1a; 使用浮动标签&#xff0c;可以在 input 输入框内插入标签&#xff0c;在单击 input 输入框时使它们浮动到上方 Bootstrap 实例 <div class"form-floating mb-3 mt-3"> <input ty…

uni.requestPayment使用

前言&#xff1a;由于uni.requestPayment没有封装H5支付方法&#xff0c;我自己封装了统一方法可以多端适用。 代码如下&#xff1a; 有两种方法&#xff1a; 方法1&#xff1a;安装 jweixin-module 包&#xff0c;我的是1.6.0版本->引入->使用 方法2&#xff1a;使用微…

51单片机实训day2——创建Keil工程(一)

以管理员身份打开软件&#xff01;&#xff01;&#xff01;以管理员身份打开软件&#xff01;&#xff01;&#xff01;以管理员身份打开软件&#xff01;&#xff01;&#xff01; 内 容&#xff1a;Keil工程创建 学 时&#xff1a;2学时 知识点&#xff1a;开发固件库介绍…

【Linux】进程替换与shell的模拟实现

目录 一、进程替换 1.1 进程替换的概念 1.2 替换函数 二、命令行解释器-Shell 2.1 shell的实现与运行 2.2 步骤讲解 一、进程替换 1.1 进程替换的概念 当我们使用 fork 函数创建子进程后&#xff0c;父子进程各自执行父进程代码的一部分。那如果创建的子进程想要执行一…

UG/NX二次开发Siemens官方NXOPEN实例解析—2.4 File2Points

列文章目录 UG/NX二次开发Siemens官方NXOPEN实例解析—2.1 AssemblyViewer UG/NX二次开发Siemens官方NXOPEN实例解析—2.2 Selection UG/NX二次开发Siemens官方NXOPEN实例解析—2.3 Selection_UIStyler UG/NX二次开发Siemens官方NXOPEN实例解析—2.4 File2Points 前言 随着…

Python开发的编译神器PyCharm----测试从业来编写Python脚本最钟意的工具

目录 前言&#xff1a; 一、PyCharm简介 二、PyCharm下载与安装 1、下载 2、安装 三、PyCharm新增Python项目 步骤1、新增 步骤2、路径配置 步骤3、环境选择 步骤4、 项目运行 四、画圣诞树 前言&#xff1a; 本文将为大家介绍PyCharm下载安装与初步的使用&#xff0c;初…

外汇天眼:美国11月独栋房屋建设跌至两年半低点!

美国商务部周二公布了这份令人沮丧的报告&#xff0c;此前周一有消息称&#xff0c;12月份房屋建筑商信心连续第12个月出现创纪录的下滑。美国11月独栋房屋建设跌至两年半低点&#xff0c;未来建筑许可大幅下滑&#xff0c;因抵押贷款利率上升继续打压楼市活动。 具体数据显示&…

(十九)Vue之组件和模块概念

文章目录传统编程组件化编程模块模块化组件化Vue学习目录 上一篇&#xff1a;&#xff08;十八&#xff09;Vue之生命周期 传统编程 一个html引入大量的CSS、JS文件&#xff0c;使得结构混乱、代码复用率低 传统方式编写应用存在的问题&#xff1a; 1.依赖关系混乱、不好维护…

某讯滑块验证码反汇编分析-第三章

某讯滑块验证码反汇编分析-第三章collect明文处理vData明文处理collect明文处理 上一篇已经跟到明文的拼接函数&#xff0c;接下来看看get函数的返回值&#xff0c;对什么进行拼接。 在这一次运行中&#xff0c;第一次get返回的是一个10&#xff0c;是怎么计算出来的呢&#x…

[XCTF]red_green(难度2)

得到一个乱码的图片&#xff0c;查看信息&#xff0c;联系“red_green” 目录 前言 一、题目重述 二、解题思路 1.套路来一遍 2.过程中注意到以下两点&#xff1a; &#xff08;1)stegsolve图层中只有red/green&#xff0c;与题目暗合 &#xff08;2)zsteg查看隐写信息发…

springboot+jsp实验室管理系统fu1ju-

目录 第一章 绪论 5 1.1 研究背景 5 1.2系统研究现状 5 1.3 系统实现的功能 6 1.4系统实现的特点 6 1.5 本文的组织结构 6 第二章开发技术与环境配置 7 2.1 Java语言简介 7 2.2JSP技术 8 2.3 MySQL环境配置 8 2.4 MyEclipse环境配置 9 2.5 mysql数据库介绍 9 2.6 B/S架构 9 第三…

2022年度安徽省职业院校技能大赛中职组“网络搭建与应用”赛项竞赛试题

2022年度安徽省职业院校技能大赛 中职组“网络搭建与应用” 赛项竞赛 &#xff08;总分1000分&#xff09; 竞赛说明 一、竞赛内容分布 “网络搭建与应用”竞赛共分三个部分&#xff0c;其中&#xff1a; 第一部分&#xff1a;网络搭建及安全部署项目&#xff08;500分&…

SpringCloud-Feign远程调用

&#x1f341;Feign的介绍 Feign是一个声明式的http客户端&#xff0c;官方地址: https://github.com/0penFeign/feign作用就是帮助我们优雅的实现http请求的发送&#xff0c;解决上面提到的问题。 &#x1f341;定义和使用Feign客户端 1.引入依赖 在order-service服务的po…

【PyTorch Geometric】工具包安装部署 过程记录(图模型学习 需要安装的工具包);图神经网络 工具包安装过程记录

目录 部署过程下载别的版本试试部署过程 在常规的安装过程中,若直接通过pip,老师讲 大概率无法成功安装。比较正规的安装方法是,下载好工具包,手动安装。 首先通过 Jupyter Notebook打开配置文档: 有些步骤在该文档中。 点击上图中的超链接,进入对应的 GitHub。 进去…

Linux进程间通信(一):匿名管道的原理和使用

文章目录一、前言二、什么是匿名管道&#xff1f;三、匿名管道的原理三、匿名管道的创建四、匿名管道实现数据传输五、匿名管道实现进程控制六、匿名管道特点总结一、前言 &#xff08;在阅读本文前&#xff0c;需要具备Linux基础IO的基本知识&#xff09;  在某些特定情况下…

【Linux】Linux下基本指令(二)

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《Linux》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1. Linux基本指令&#xff1a;&#xff08;续&#xff09; 1.1man指令&am…

英国访问学者AV签证超期怎么办?

英国AV签证只给12个月整&#xff0c;关于 AV签证超期怎么办&#xff1f;知识人网访问学者老师和大家聊一聊&#xff1a; 一、呆不满一年(360天)会遇到的问题主要有&#xff1a; 1、无法申请留学回国科研启动基金。 2、需要向CSC返还一定的资助费用。 3、国内一些地方判断一个…

数字化转型如何认清本质少被忽悠:小步快跑看到项目效果再推下一步

这些年&#xff0c;我们见识了太多新概念&#xff1a;数据智能、Data Fabric、数据虚拟化还有最著名的“数据中台”。 然而&#xff0c;跟几年前疯狂追逐这些热词不同&#xff0c;或者说因为已经踩坑踩到晕厥。动辄一个千万的中台项目投入进去&#xff0c;上线后高层看数还是要…

【网络安全】Redis未授权访问查看敏感文件

前言 什么是未授权访问漏洞呢&#xff1f;也就是没有权限&#xff0c;但是可以访问站点或者服务器的漏洞&#xff0c;称之为未授权访问漏洞&#xff0c;这个属于高危漏洞。比如我们常见的路由器未授权访问等等。 一、准备阶段 service httpd.service start 开启apache服务se…

【文档编辑】积累

文章目录WordExcel填充表格样式函数Markdownsublime搜索jsonhexWord Word 2016 撰写论文(1): 公式居中、编号右对齐 Excel 填充 EXCEL excel中运用ctrlD、ctrlenter、ctrlE批量填充数据 表格样式 Excel技法&#xff1a;三招实行Excel隔行填色 函数 COUNTA 函数 step1&…