Java并发编程第10讲——CAS相关知识点详解

news2025/1/11 17:47:11

前面介绍锁的时候顺便也提到了CAS,但作为JUC的“基石”和面试中的高频考点,还是不够。所以,本篇文章将从CAS的概念入手,逐步深入介绍12个Atomic原子操作类、CAS的实现原理(源码解析)、Unsafe类、CAS存在的问题以及LongAddr

一、什么是CAS

CAS全称Compare And Swap,顾名思义就是先比较再交换。主要应用就是实现乐观锁和锁自旋。CAS操作包含三个操作数——内存位置(V)、预期值(A)和新值(B)。在并发修改的时候,会先比较A和V的值是否相等,如果相等,则会把值替换成B,否则就不做任何操作。

当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程不会被挂起,而是被告知这次竞争失败,并可以再次尝试。

在JDK1.5中新增的java.util.concurrent(JUC)就是建立在CAS之上的。相对于synchronized这种阻塞型的同步,CAS是非阻塞算法的一种常见实现,所以JUC在性能上有了很大的提升。

二、举个例子

我们先看一个经典的例子:

public class Demo {
    public static volatile int i=0;
    public static void increase(){
        i++;
    }
    public static void main(String[] args) throws InterruptedException {
        //启动10个线程,每个线程累加10000次
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }, String.valueOf(j)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最后i的值为:" + i);
    }
}

这个例子在Java并发编程第5讲——volatile关键字中也用过,启动10个线程,每个线程累加10000次,那么i的预期的结果是100000,但运行完这段代码得到的结果都是小于100000的数字。

这是因为volatile只能保证可见性,无法保证原子性,而自增操作并不是一个原子操作,它会出现写回主存覆盖的问题,送一我们每次都无法获得想要的结果,那么如何解决呢?说白了,这也就是线程安全问题,那么我们可以从保障线程安全入手,以下提供几个方案:

  • synchronized:线程内使用同步代码块,由JVM自身的机制来保障线程的安全性。
  • Lock锁:基于AQS+CAS实现。
  • Atomic原子类:这就是我们今天的主角,下面再细说。
  • LongAdder原子类:和Atomic原子类在同一包下,不过LongAdder更适合于高并发场景下,特别是写大于读的场景,相较于Atomic性能更好,代价是消耗更多的空间,以空间换时间。(下面也会细说)。

三、12个Atomic原子操作类

Java从JDK 1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

因为变量的类型由很多种,所以在Atomic包里一共提供了12个类,可以分为4种类型,分别是基本类型、数组、引用、字段。Atomic包里的类基本都是使用Unsafe实现的包装类。

3.1 基本类型(3个)

  • AtomicBoolean:布尔类型
  • AtomicInteger:整型类型
  • AtomicLong:长整型类型

它们三个提供的方法基本一样,所以就以AtomicInteger为例,下面是它常用的方法:

  • AtomicInteger(int initialValue):构造方法,设置初始值,若不设置则初始值为0。
  • int getAndIncrement():以原子方式进行+1操作,并返回自增前的值。
  • int IncrementAndGet():以原子方式进行+1操作,并返回自增后的值。
  • int decrementAndGet():以原子方式进行-1操作,并返回自减后的值。
  • int get():返回当前值。
  • int addAndGet(int delta):以原子方式将输入的值于当前值相加,并返回结果。
  • boolean compareAndSet(int expect,int update):如果输入的值(expect)等于AtomicInteger的当前值,则以原子的方式将该值设置为输入的值(update),成功返回ture,反之返回false。

现在,我们可以用AtomicInteger来改造一下上面的代码,使其正常运行:

public class Demo {
    public static AtomicInteger i=new AtomicInteger();
    public static void increase(){
        i.getAndIncrement();
    }
    public static void main(String[] args) throws InterruptedException {
        //启动10个线程,每个线程累加1000次
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }, String.valueOf(j)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最后i的值为:" + i.get());//100000
    }
}

我们把i改成使用AtomicInteger定义,i++改为“i.incrementAndGet()”使其变成原子操作,所以我们可以确保每次都可以获得正确的结果,并且性能上也有了不错的提升。

3.2 数组类型(3个)

通过原子的方式更新数组里的某个元素。

  • AtomicIntegerArray:整型数组
  • AtomicLongArray:长整型数组
  • AtomicReferenceArray:引用类型数组

这三个提供的方法也基本一样,下面以AtomicIntegerArray为例,先看一下它的常用方法:

  • AtomicIntegerArray(int length):构造方法,创建指定长度的AtomicIntegerArray对象,所有元素的初始值都是0。
  • AtomicIntegerArray(int[] array):构造方法,使用指定的整型数组初始化AtomicIntegerArray对象,数组的长度就是AtomicIntegerArray的长度。
  • int get(int index):获取指定索引处的元素值。
  • int length:返回数组的长度。
  • void set(int index,int newValue):设置指定索引处的新值。
  • int getAndIncrement(int index):获取指定索引处的旧值,并将其+1。
  • int getAndDecrement(int index):获取指定索引处的旧值,并将其-1。
  • int getAndAdd(int index,int delta):获取指定索引处的旧值,并将其加delta。

举个例子:

public class ArrayDemo {
    public static void main(String[] args) {
        //指定长度为3的AtomicIntegerArray对象,初始值都是0
        AtomicIntegerArray array=new AtomicIntegerArray(3);
        //获取索引为0的元素值
        System.out.println(array.get(0));//0
        //给索引为1的元素值设置为5
        array.set(1,5);
        System.out.println(array.get(1));//5
    }
}

3.3 引用类型(3个)

如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。

  • AtomicReference:原子更新引用类型。
  • AtomicStampedReference:在AtomicReference的基础上增加了版本号的功能。
  • AtomicMarkableReference:在AtomicReference的基础上增加了布尔标记的功能。

下面就以AtomicReference为例,常用方法:

  • AtomicReference(V initialValue):构造方法,创建一个AtomicReference对象,并将初始值设置为initialValue。
  • V get():获取当前的引用值。
  • void set(V newValue):设置新的引用值。
  • boolean compareAndSet(V expect,V update):如果引用值于期望值相等,则设置update为新值。
  • V getAndSet(V newValue):设置新的引用,并返回旧的引用。

举个例子:

public class ReferenceDemo {
​
    public static void main(String[] args) {
        Student student = new Student("张三", 20);
        //创建一个指定引用为Student的AtomicReference对象
        AtomicReference<Student> reference=new AtomicReference<>(student);
        //获取当前引用
        System.out.println(reference.get());//ReferenceDemo.Student(name=张三, age=20)
        //修改student引用
        student.setAge(18);
        student.setName("小黑子");
        //设置新的引用
        reference.set(student);
        System.out.println(reference.get());//ReferenceDemo.Student(name=小黑子, age=18)
    }
​
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    static class Student{
        private String name;
        private int age;
    }
}

3.4 字段类型(3个)

如果需要原子地更新某个类的某个字段时,那么就可以用原子更新字段类。

  • AtomicIntegerFieldUpdater:整型,可以在指定的对象上进行原子更新操作,支持原子地设置、获取、增加、减少等操作。
  • AtomicLongFieldUpdater:长整型,可以在指定的对象上进行原子更新操作,支持原子地设置、获取、增加、减少等操作。
  • AtomicReferenceFieldUpdater:引用类型,可以在指定的对象上进行原子更新操作,支持原子地设置、获取、增加、减少等操作。

下面以AtomicIntegerFieldUpdater为例,常用方法:

  • AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,String fieldName):用于操作指定类种的指定字段。
  • int get(T obj):获取指定对象的字段的当前值。
  • void set(T obj,int newValue):设置指定对象的字段的值为指定的新值。
  • int getAndIncrement(T obj):以原子的方式将指定对象的字段+1,并返回旧值。
  • int getAndDecrement(T obj):以原子的方式将指定对象的字段-1,并返回旧值。

举个例子:

@Data
public class IntegerUpdaterDemo {
    //字段必须是 volatile 类型的
    private volatile int score;
    //创建AtomicIntegerFieldUpdater 实例
    private static final AtomicIntegerFieldUpdater<IntegerUpdaterDemo> SCORE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(IntegerUpdaterDemo.class, "score");
​
    public static void main(String[] args) {
        IntegerUpdaterDemo demo = new IntegerUpdaterDemo();
        demo.setScore(80);
        //使用AtomicIntegerFieldUpdater进行原子性更新操作
        int newValue = 90;
        int oldValue = SCORE_UPDATER.getAndSet(demo, newValue);
        System.out.println("原始值: " + oldValue + ", 更新后的值: " + demo.getScore());  //原始值: 80, 更新后的值: 90 
    }
}

四、CAS实现原理

下面以AtomicInteger原子整型类为例,分析一下CAS底层实现机制。

4.1 AtomicInteger的getAndIncrement方法

上面解决i++问题的时候用到了AtomicInteger的getAndIncrement方法,通过方法调用,可以发现getAndIncrement方法调用了getAndAddInt方法,最后调用的是Unsafe类的compareAndSwapInt方法,也就是我们今天要介绍的CAS:

compareAndSwapInt方法参数解释:

  • var1:Unsafe对象本身,需要通过这个类获取value的内存偏移地址。
  • var2:value变量的内存偏移地址。
  • var4:需要增加或减少的数。
  • var5:内存位置的最新值。
  • var5+var4:要更新的最新值。

就是拿到内存位置的最新值var5,使用CAS尝试将内存位置的值修改为目标值var5+var4,如果修改失败,则获取改内存位置的新值var5,然后继续尝试,直到成功。

4.2 Unsafe类

读到这我们知道Java中Atomic类的实现都是基于Unsafe类实现的,那么Unsafe类是什么呢?

4.2.1 什么是Unsafe类

Unsafe类是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过JVM还是开了一个后门,JDK中有一个Unsafe类,他提供了硬件级别的原子操作,说白了Unsafe类就是跟底层硬件CPU指令通讯的复制工具。

Unsafe类包含了很多基础操作,比如数组操作、对象操作、内存操作、CAS操作、线程(park)操作,栅栏(Fence)操作,JUC包以及一些三方框架都是用Unsafe类来保证线程安全。

Unsafe类在jdk源码的多个类中用到,这个类提供了一些绕开JVM的更底层的功能,基于它的实现可以提高效率。不过,正如它名字那样——不安全类,他所分配的内存需要手动free(不会被GC回收)。

Unsafe类提供了硬件级别的操作,主要有以下功能:

  • 通过Unsafe类可以分配内存,可以释放内存。
  • 可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的。
  • 将线程进行挂起与恢复。
  • CAS操作。

4.2.2 举个例子

Unsafe被设计的初衷,并不是希望被一般开发者调用,它的构造方法是私有的,所以我们不能通过new或工厂方法实例化Unsafe对象,通常用反射来获取Unsafe示例:

private static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
    //Unsafe类中提供了一个静态的getUnsafe方法,可以返回一个Unsafe实例
    Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
    unsafeField.setAccessible(true);
    return (Unsafe) unsafeField.get(null);
}

来看一下分配内存的例子:

public class UnsafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafeInstance();
        //分配一个包含10个整数的连续内存空间
        long size = 10 * Integer.BYTES;
        //分配内存
        long memoryAddress = unsafe.allocateMemory(size);
        //在内存中存储值
        for (int i = 0; i < 10; i++) {
            unsafe.putInt(memoryAddress + i * Integer.BYTES, i);
        }
        for (int i = 0; i < 10; i++) {
            //从内存中读取值
            int value = unsafe.getInt(memoryAddress + i * Integer.BYTES);
            System.out.println(value);
        }
        //释放内存
        unsafe.freeMemory(memoryAddress);
    }
    //获取Unsafe实例
    private static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        return (Unsafe) unsafeField.get(null);
    }
}

同样,也可以用Unsafe类实现一个CAS操作,这里就不做演示了。

4.2.3 Unsafe类的compareAndSwapInt方法

介绍完Unsafe类,我们书接4.1,接着继续深入探讨该方法,该方法在Unsafe中对应的C++源码如下:

可以看到调用了Atomic::cmpxchg方法,那么我们再来看看Atomic::cmpxchg方法:

标红框的地方就是调用了汇编指令cmpxchg,作用是比较并交换而CAS操作的原理就是基于硬件提供的原子操作指令——cmpxchg指令实现的:

  • cmpxchg指令是一条原子指令。在cpu执行cmpxchg指令时,处理器会自动锁定总线,防止其它cpu刚问共享变量,然后执行比较和交换操作,最后释放总线。
  • cmpxchg指令在执行期间,cpu会自动禁止中断。这样可以确保CAS操作的原子性,避免中断或其它干扰对操作的影响。
  • cmpxchg指令时硬件实现的,可以保证其原子性和正确性。cpu中的硬件电路确保了cmpxchg指令的正确执行,以及对共享变量的访问原子性。

ps:同样是因为cmpxchg指令,这个指令基于cpu缓存一致性协议实现的。在多个cpu中,所有核心的缓存都是一致的。当一个cpu核心执行cmpxchg指令时,其它cpu核心的缓存会自动更新,以确保对共享变量的访问是一致的。

五、CAS存在的问题

任何技术都不是十全十美的,强如硬件级别实现的CAS操作也会有瑕疵。

5.1 ABA问题

CAS算法实现一个重要前提是需要取出内存中某时刻的数据,而在下个时刻进行比较和交换,那么这个时间差会导致数据的变化。

比如,当线程1要修改A时,会先读取A的值,如果此时有一个线程2,经过一系列操作,将A修改为B,再由B修改为A,然后线程1在比较A时,发现A的值没有改变,于是就修改了。但此时A的版本已经不是最先读取的版本了,这就时ABA问题。

如何解决?解决这个问题的办法也很简单,就是添加版本号,修改数据时带上一个版本号,如果版本号我数据的版本号一致就修改(同时修改版本号),否则就失败,或者改用传统的互斥同步方式。

5.2 忙等待问题

上面的getAndAddInt方法就是一个很好的例子,getAndAddInt方法执行时,如果CAS失败,会一直尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

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

当对一个共享变量操作时,我们可以用CAS的方式来保证原子操作,但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候我们可能就会用锁来保证原子性。

六、LongAdder

在5.2中提到的忙等待问题会给CPU带来很大的开销,从而会使性能下降。当然在大部分场景下使用Atomic原子操作类还是绰绰有余的,但在多线程竞争比较激烈,也就是并发冲突比较大的情况下,就得考虑其它方案了,比如LongAddr。

6.1 什么是LongAddr

LongAddr是Java 8中推出的一个新的类,主要是为了解决Atomic原子操作类在多线程竞争激烈的情况下性能并不高的问题,它主要是采用分段+CAS的方式来提升原子操作的性能。

但是性能高就意味着要有牺牲,而LongAddr就是典型的以空间换时间的实现方式,所以它需要用到更大的空间,而且LongAddr还可能存在结果不准确的问题,Atomic原子操作类并不会。

将AtomicInteger的例子改造一下:

public class LongAddrDemo {
    public static LongAdder i=new LongAdder();
    public static void increase(){
        i.increment();
    }
    public static void main(String[] args) throws InterruptedException {
        //启动10个线程,每个线程累加1000次
        for (int j = 0; j < 10; j++) {
            new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    increase();
                }
            }, String.valueOf(j)).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(i.sum());//100000
    }
}

6.2 LongAddr实现原理(源码解析)

Java 8中的LongAddr类继承自抽象类Striped64(也是atomic包下的),这个类也是Java 8新增的,主要的作用就是给并发场景下提供计数支持。

Striped64的设计思路与ConcurrentHashMap类似,都是通过分散竞争的方式来提升并发性能,其中主要依赖了两个字段:

解释:

  • cells数组:当检测到有base变量被线程占有时,就会尝试暂存在cells数组中。
  • base变量:当没有线程竞争时,首先会改变base的值。

最终的结果:base变量+cells数组中value。

通过查看LongAddr中的add方法的代码,我们就能更好地理解:

解释:

  • 外层if:判断是否需要通过base进行累加

    • 如果cells数组!=null,说明已经存在分段累加器(cells数组),则不需要base进行累加。

    • 如果cells数组==null,即不存在分段累加器,那么就尝试使用cas进行base累加,失败则进入if进行更细粒度的累加。

  • 内层if:进一步判断是否需要进行细粒度的累加操作

    • 再一次判断cells数组是否为空,为空则表示没有分段的累加器。不为空进入下一步。

    • cells数组长度m是否小于0。不小于0进入下一步。

    • getProbe()&m通过探针索引获取cells数组中的某个位置的cell对象a,看是否有初始化。不为空进入下一步。

    • 将Cell对象a的value与v+x进行CAS操作。

满足以上四个判断之一,则调用longAccumulate方法进行细粒度的累加操作。

总结一下,首先尝试通过CAS更新base的值,如果再竞争不激烈的情况下,是可以CAS成功的,如果成功,那么就和Atomic原子操作类一样了。如果失败,那么就代表竞争激烈,那么就会尝试通过cells数组来分散计数。Striped64根据线程来计算哈希,然后将不同的线程分散到不同Cell数组的index上,然后这个线程的计数内容就会保存在该Cell的位置上面,最后总计数则是base加上cells数组中的计数内容。

这跟ConcurrentHashMap的分段锁很像吧。最后再来看看它是怎么统计的:

很简单,就是将base和cells数组中的值都加起来得到最终的结果。

6.3 LongAddr计算不准确的问题

还是那句话,没有十全十美的技术,那么LongAddr为什么会存在计算不准确的情况呢?

通过查看LongAddr的代码很容易可以发现一个问题,那就是LongAddr的累加结果可能是不准确的,而且sum这个方法的注释上也明确点处了这个问题:

Returns the current sum. The returned value is NOT an atomic snapshot; invocation in the absence of concurrent updates returns an accurate result, but concurrent updates that occur while the sum is being calculated might not be incorporated.

翻译过来就是:返回值不是原子快照;在没有并发更新的情况下调用会返回准确的结果,但是在计算总和时发生并发更新可能不会被纳入计算。举个极端的例子:在并发高的情况下,如果刚好返回sum的时候有其它原子操作进行了累加操作,那么这时候返回的sum就会不准。

总体来说,LongAddr在高并发情况下提供了更好的性能,但牺牲了计算的准确性,所以它更适合应用于并发竞争激烈,但对数据准确度要求并不是百分之百准确的场景,比如微博点赞、文章阅读量的统计等场景。

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

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

相关文章

Oracle通过透明网关查询SQL Server 报错ORA-00904

Oracle通过透明网关查询SQL Server 报错ORA-00904 问题描述&#xff1a; 只有全表扫描SELECT * 时SQL语句可以正常执行 添加WHERE条件或指定列名查询&#xff0c;查询语句就报错 问题原因&#xff1a; 字段大小写和SQLSERVER中定义的不一致导致查询异常 解决办法&#xff1a; 给…

JVM——堆内存调优(Jprofiler使用)Jprofile下载和安装很容易,故没有记录,如有需要,在评论区留言)

堆内存调优 当遇到OOM时&#xff0c;可以进行调参 1、尝试扩大堆内存看结果 2、分析内存&#xff0c;看哪个地方出现了问题&#xff08;专业工具&#xff09; 调整初始分配内存为1024M&#xff0c;调整最大分配内存为1024M&#xff0c;打印GC细节&#xff08;如何添加JVM操…

「快学Docker」Docker镜像和容器的创建与管理

「快学Docker」Docker镜像和容器的创建与管理 引言什么是Docker镜像&#xff1f;镜像获取和使用镜像获取镜像使用 什么是Docker容器&#xff1f;Docker容器与主机之间的交互基于Dockerfile创建镜像基于镜像创建容器总结 引言 Docker镜像和容器是当今云计算领域中最受欢迎的技术…

【Buildroot】记一次编译出错gzip: popt-1.16.tar.gz: not in gzip format--更改br里面的默认下载地址

文章目录 我在一次正常的编译过程中遇到了&#xff0c;如下的错误&#xff1a; rootubuntu:/home/liefyuan/Linux/rk356x_linux/buildroot# make -j16 make: Circular /home/liefyuan/Linux/rk356x_linux/buildroot/output/build/iproute2-4.14.1/.stamp_configured <- bus…

Trie树/字典树的原理及实现[C/C++]

文章目录 前言引例&#xff1a;Google经典面试题字典树的原理与实现定义字典树的结构字典树的操作字符串插入字符串查询 字典树的实现字符集数组法节点类结构设计节点的接口字符映射节点类的代码实现 字典树类结构设计字典树接口实现 字符集映射法&#xff08;适用性广&#xf…

idea使用Alibaba Cloud Toolkit实现自动部署

在日常开发过程中&#xff0c;经常会使用到jenkins进行项目部署&#xff0c;但对一些小项目来说&#xff0c;这就过于复杂&#xff0c;就可以使用Alibaba Cloud Toolkit插件配合shell脚本进行项目的远程部署工作。 一、下载Alibaba Cloud Toolkit插件 二、服务器安装nohup 1.…

数据分享 I 地级市人口和土地使用面积基本情况

数据地址&#xff1a; 地级市人口和土地使用面积基本情况https://www.xcitybox.com/datamarketview/#/Productpage?id394 基本信息. 数据名称: 地级市人口和土地使用面积基本情况 数据格式: ShpExcel 数据时间: 2021年 数据几何类型: 面 数据坐标系: WGS84坐标系 数据…

手把手实现简易版vue(二)组件类解析

1、构造器 constructor(propsArgus {}) {const {data () > {},methods {},watch {},computed {}, // 待实现props {}, // 待实现created () > {}, // created钩子函数mounted () > {}, // mounted钩子函数destroyed () > {} // destroyed钩子函数} props…

MySQL云数据库5.5导入到自建MySQL数据库5.7

有一个MySQL云数据库&#xff0c;版本比较老&#xff0c;是5.5. 需要在线下搭建一个测试环境&#xff0c;所以需要将数据还原到一个自建MySQL数据库内。 5.5已经很难找到了&#xff0c;所以安装了一个5.7. 云数据库设置的备份&#xff0c;使用的是全复制文件方法。 还原数据…

电源效率测试标准你知道多少?纳米软件带您了解

电源效率是衡量电源能源利用率和电源质量的重要指标&#xff0c;是电源模块测试的一个重要测试项目。对于电源效率各个国家都有自己的标准&#xff0c;以此来判断能量转换效率&#xff0c;促进提升能源利用率。 什么是电源效率标准? 电源效率标准是衡量电源能量转换率的指标&a…

Spark-Core

Spark简介 Spark-Core核心算子 Spark-Core 文章目录 一、RDD 编程1、RDD序列化1.2 Kryo序列化框架 2、RDD依赖关系2.1 查看血缘关系2.2 查看依赖关系2.3 窄依赖2.4 宽依赖2.5 Stage任务划分 3、RDD 持久化3.1 Cache缓存3.2 CheckPoint检查点3.3 缓存和检查点区别3.4 检查点存储…

天锐绿盾加密软件——企业数据透明加密、防泄露系统

天锐绿盾是一种企业级数据透明加密、防泄密系统&#xff0c;旨在保护企业的核心数据&#xff0c;防止数据泄露和恶意攻击。它采用内核级透明加密技术&#xff0c;可以在不影响员工正常工作的前提下&#xff0c;对需要保护的数据进行加密操作。 PC访问地址&#xff1a; https:/…

基于springboot基于会员制医疗预约服务管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot基于会员制医疗预约服务管理系统演示 摘要 会员制医疗预约服务管理信息系统是针对会员制医疗预约服务管理方面必不可少的一个部分。在会员制医疗预约服务管理的整个过程中&#xff0c;会员制医疗预约服务管理系统担负着最重要的角色。为满足如今日益复杂的管理需…

公司新品上市,如何做好新品发布会宣传

公司新品上市不仅展现了公司的生命力与活力&#xff0c;还代表了公司与时俱进的创新力&#xff0c;积极听取用户反馈的服务精神&#xff0c;而公司新品上市时都会举办新品发布会&#xff0c;今天媒介盒子就来和大家分享&#xff0c;公司如何做好新品发布会的宣传。 一、 撰写活…

2023年中国潜水电机行业现状及前景分析[图]

潜水电机是一种特殊设计的电动机&#xff0c;通常用于水下应用。它们被设计成能够在液体环境中工作&#xff0c;通常是在水中或其他液体中&#xff0c;而且能够在潜水的情况下继续正常运行。潜水电机通常具有防水性能和耐腐蚀性&#xff0c;以适应恶劣的水下环境。 潜水电机行…

Java实现连接SQL Server解决方案及代码

下面展示了连接SQL Server数据库的整个流程&#xff1a; 加载数据库驱动建立数据库连接执行SQL语句处理结果关闭连接 在连接之前&#xff0c;前提是确保数据库成功的下载&#xff0c;创建&#xff0c;配置好账号密码。 运行成功的代码&#xff1a; import java.sql.*;publi…

点集合的三角剖分

点集合的三角剖分是指如何将一些离散的点集合组合成不均匀的三角形网格&#xff0c;使得每个点成为三角网中三角面的顶点。这个算法的用处很多&#xff0c;一个典型的意义在于可以通过一堆离散点构建的TIN实现对整个构网区域的线性控制&#xff0c;比如用带高程的离散点构建的T…

Windows网络监视工具

对于任何规模的企业来说&#xff0c;网络管理在信息技术中都起着至关重要的作用。管理、监控和密切关注网络基础设施对任何组织都至关重要。在Windows网络中&#xff0c;桌面&#xff0c;服务器&#xff0c;虚拟服务器和虚拟机&#xff08;如Hyper-V&#xff09;在Windows操作系…

医院电力系统智能能效监控平台的应用

0引言 随着社会和科学技术的发展&#xff0c;配电系统的智能化已经成为一种发展趋势。医院建设电力智能监控平台&#xff0c;可对供电系统进行集中管理和调度、实时控制和数据采集&#xff0c;监控供电系统设备的运行情况&#xff0c;及时掌握和处理供电系统的各种事故、报警事…