【JavaEE精炼宝库】多线程进阶(1)常见锁策略 | CAS | ABA问题

目录

一、常见的锁策略:

1.1 悲观锁 | 乐观锁:

1.2 重量级锁 | 轻量级锁:

1.3 自旋锁 | 挂起等待锁:

1.4 公平锁 | 非公平锁:

1.5 可重入锁 | 不可重入锁:

1.6 互斥锁 | 读写锁:

1.7 面试题:

二、CAS

2.1 CAS 的概念:

2.2 CAS 的实现的:

2.3 CAS 的应用:

2.3.1 实现原子类:

2.3.2 实现自旋锁:

2.4 CAS 的 ABA 问题:

2.4.1 ABA 问题的概述:

2.4.2 ABA 问题引来的 BUG:

2.5 解决方案:

2.6 面试题:


终于进入到多线程的进阶了,这里面涉及到的内容面试容易考,但是工作中很少直接用到。

一、常见的锁策略:

注意:接下来讲解的锁策略不仅仅是局限于 Java 。任何和 "锁" 相关的话题,都可能会涉及到以下内容。这些特性主要是给锁的实现者来参考的。我们了解一些,也能更加合理的使用锁。

1.1 悲观锁 | 乐观锁:

加锁的时候,预测当前锁冲突的概率是大还是小。

• 悲观锁:

预测当前锁的冲突概率大,后续要做的工作往往就会更多。加锁的开销就会更大(时间,系统资源)。

• 乐观锁:

预测当前锁的冲突概率不大,后续要做的工作往往就会更少。加锁的开销就会更小(时间,系统资源)。

synchronized 初始使用乐观锁策略。当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。所以 synchronized 既是乐观锁也是悲观锁,支持自适应。

1.2 重量级锁 | 轻量级锁:

一般来说,悲观锁往往就是重量级锁(加锁过程做的事情多),乐观锁往往就是轻量级锁(加锁过程做的事情少)。

锁的核心特性 "原子性",这样的机制追根溯源是 CPU 这样的硬件设备提供的。硬件有提供,软件层面才能实现。

• CPU 提供了 "原子操作指令"。

• 操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁。

• JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类。

注意:synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其他的工作。  

• 重量级锁: 

加锁机制重度依赖了 OS 提供了 mutex。

这样做的特点有:1. 大量的内核态用户态切换。2. 很容易引发线程的调度。

这两个操作,成本比较高,一旦涉及到用户态和内核态的切换,就意味着 “沧海桑田”。 

• 轻量级锁:

加锁机制尽可能不使用 mutex,而是尽量在用户态代码完成,实在搞不定了,再使用 mutex。

这样做的特点有:1. 少量的内核态用户态切换。2. 不太容易引发线程调度。

为什么会有这样的好处呢?举个栗子:

想象去银行办业务。在窗口外,自己做,这是用户态,用户态的时间成本是比较可控的。在窗口内让工作人员做,这是内核态,内核态的时间成本是不太可控的(可能人家处理一半,去做别的事情了)。如果办业务的时候反复和工作人员沟通,还需要重新排队,这时效率是很低的。

重量级锁、轻量级锁和悲观锁、乐观锁的概念有重合的地方,面试的时候要能转的过来。

synchronized 开始是一个轻量级锁。如果锁冲突严重,就会变成重量级锁。

1.3 自旋锁 | 挂起等待锁:

• 自旋锁:

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题。

自旋锁伪代码:

while (抢锁(lock) == 失败) {}

⼀旦锁被其他线程释放,就能第⼀时间获取到锁(线程没有被调度)。

自旋锁是一种典型的轻量级锁的实现方式。

优点:没有放弃 CPU,不涉及线程阻塞和调度,⼀旦锁被释放,就能第一时间获取到锁。

缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源,CPU 在空转。

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。

• 挂起等待锁:

是重量级锁的一种典型的实现方式,借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(阻塞状态)。此时这个线程就不会参与线程调度了。知道这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁。

1.4 公平锁 | 非公平锁:

• 公平锁:遵守 "先来后到"。B 比 C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。

• 非公平锁:不遵守 "先来后到"。B 和 C 都有可能获取到锁。

其实这两个策略都挺公平的,只是最初的 Java 大佬把先来后到定义成公平,均等机会定义成不公平。

注意:

• 操作系统内部的线程调度就可以视为是随机的。如果不做任何额外的限制,锁就是非公平锁。如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序。

• 公平锁和非公平锁没有好坏之分,关键还是看业务场景。

synchronized 非公平锁。

1.5 可重入锁 | 不可重入锁:

可重入锁的字面意思是 “可以重新进入的锁”,即允许同一个线程多次获取同一把锁。

例如:一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。 

Java里只要以 Reentrant 开头命名的锁都是可重入锁,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。而 Linux 系统提供的 mutex 是不可重入锁。synchronized 是可重入锁。这个前面几篇文章有涉及到,这里就不再赘述。

1.6 互斥锁 | 读写锁:

我们平时见到的 synchronized 是普通的互斥锁,读写锁是更加特殊的存在。

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

Java 的读写锁是这样设定的:

• 读锁和读锁之间,不会产生互斥。

• 写锁和写锁之间,会产生互斥。

• 读锁和写锁之间,会产生互斥。

突出体现的是 “读操作和读操作” 之间是共享的(不会互斥),有利于降低锁冲突的概率,提高并发能力。

注意:和之前谈到的数据库中的事务,给读操作加锁:读的时候不能写。给写操作加锁:写的时候不能读。不是一回事。这是在减低并发能力。

读写锁就是把读操作和写操作区分对待。Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁。

• ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。

• ReentrantReadWriteLock.WriteLock 类表示一个写锁。这个对象也提供了 lock / unlock 方法进行加锁解锁。

读写锁特别适合于 "频繁读,不频繁写" 的业务中。(这样的场景其实也是非常广泛存在的)。

1.7 面试题:

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

答:悲观锁认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁。乐观锁认为多个线程访问同⼀个共享变量冲突的概率不大,并不会真的加锁,而是直接尝试访问数据。在访问的同时识别当前的数据是否出现访问冲突。悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex)获取到锁再操作数据。获取不到锁就等待。乐观锁的实现可以引入一个版本号。借助版本号识别出当前的数据访问是否冲突。

2. 介绍下读写锁?

答: 读写锁就是把读操作和写操作分别进行加锁。读锁和读锁之间不互斥。写锁和写锁之间互斥,写锁和读锁之间互斥,读写锁最主要用在 "频繁读,不频繁写" 的场景中。

3. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

答:如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试,会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。

相比于挂起等待锁:

• 优点:没有放弃 CPU 资源,一旦锁被释放就能第一时间获取到锁,更高效。在锁持有时间比较短的场景下非常有用。

• 缺点:如果锁的持有时间较长,就会浪费 CPU 资源。

4. synchronized 是可重入锁么?

答:是可重入锁。可重入锁指的就是连续两次加锁不会导致死锁。实现的方式是在锁中记录该锁持有的线程身份,以及⼀个计数器(记录加锁次数),如果发现当前加锁的线程就是持有锁的线程,则直接计数自增。

二、CAS

2.1 CAS 的概念:

CAS:全称Compare and swap,字面意思:”比较并交换“。一个 CAS 涉及到以下操作:我们假设内存中的原数据V,旧的预期值A,需要修改成的新值B。

1. 比较 A 与 V 是否相等。(比较)

2. 如果比较相等,将 B 写入 V。(交换)

3. 返回操作是否成功。

这是一条 CPU 指令(原子的),可以完成比较和交换。这给我们编写线程安全的代码,打开了新世界的大门。

• CAS 伪代码:

注意:下面写的代码不是原子的,真实的 CAS 是⼀个原子的硬件指令完成的。这个伪代码只是辅助理解 CAS 的工作流程。

address 是内存地址,expectValue 和 swapValue 都是寄存器的值(CPU)。 

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
 &address = swapValue;
 return true;
 }
 return false;
}

当多个线程同时对某个资源进行 CAS 操作,只能有一个线程能操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS 可以视为是一种乐观锁。(或者可以理解成 CAS 是乐观锁的一种实现方式)

2.2 CAS 的实现的:

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:

• java 的 CAS 利用的是 unsafe 这个类提供的 CAS 操作。

• unsafe 的 CAS 依赖的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg。

• Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 CPU 硬件提供的 lock 机制保证其原原性。

简而言之,是因为硬件予以了支持,软件层面才能做到。

2.3 CAS 的应用:

基本涉及到锁,程序就和高性能无缘了。这里可以为无锁编程提供一些思路(当然大部分情况下,只有加锁才行)。

2.3.1 实现原子类:

标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的。典型的就是 AtomicInteger 类。

如下案例:

import java.util.concurrent.atomic.*;
public class demo1 {
    static AtomicInteger count = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for(int i = 0;i < 50000;i++){
                    count.getAndIncrement();
                }
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

案例演示结果如下:

可以发现是线程安全的。因为这里的 ++ 操作是原子的。

伪代码实现:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

• 对上面代码执行过程的刨析:

假设两个线程同时调用 getAndIncrement:

1. 两个线程都读取 value 的值到 oldValue 中。(oldValue 是⼀个局部变量,在栈上,每个线程有自己的栈)。

2. 线程 1 先执行 CAS 操作。由于 oldValue 和 value 的值相同,直接进行对 value 赋值。

注意:CAS 是直接读写内存的,而不是操作寄存器。 CAS 的读内存,比较,写内存操作是⼀条硬件指令,是原子的。

3. 线程 2 再执行 CAS 操作,第一次 CAS 的时候发现 oldValue 和 value 不相等,不能进行赋值。因此需要进入循环。在循环里重新读取 value 的值赋给 oldValue。

4. 线程 2 接下来第二次执行 CAS,此时 oldValue 和 value 相同,于是直接执行赋值操作。

5. 线程 1 和线程 2 返回各自的 oldValue 的值即可。

通过形如上述代码就可以实现⼀个原子类。不需要使用重量级锁,就可以高效的完成多线程的自增操作。

2.3.2 实现自旋锁:

• 自旋锁伪代码:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.  
        // 如果这个锁已经被别的线程持有, 那么就⾃旋等待.  
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.  
        while(!CAS(this.owner, null, Thread.currentThread())){
        }
    }
    public void unlock (){
        this.owner = null;
    }
}

2.4 CAS 的 ABA 问题:

2.4.1 ABA 问题的概述:

假设存在两个线程 t1 和 t2。有一个共享变量 num,初始值为 A。接下来,线程 t1 想使用CAS 把 num 值改成 Z,那么就需要先读取 num 的值,记录到 oldNum 变量中。使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。

但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A。

到了这里就有个问题:线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了。只不过又改成 A 了。这个时候 t1 究竟是否要更新 num 的值为 Z ?

 

这就好比,我们买一个手机,无法判定这个手机是刚出厂的新手机,还是别人用旧了,又翻新过的手机。

2.4.2 ABA 问题引来的 BUG:

大部分的情况下,t2 线程这样的一个反复横跳改动,对于 t1 是否修改 num 是没有影响的。但是不排除一些特殊情况。

案例:假设滑稽有 100 存款。滑稽想从 ATM 取 50 块钱。取款机创建了两个线程,并发的来执行 -50 操作。我们期望一个线程执行 -50 成功,另一个线程 -50 失败。如果使用 CAS 的方式来完成这个扣款过程就可能出现问题。

• 正常的过程:

存款 100,线程 1 获取到当前存款值为 100,期望更新为 50。线程 2 获取到当前存款为 100,期望更新为 50。 线程 1 执行扣款成功,存款被改成 50。线程 2 阻塞等待中。轮到线程 2 执行,发现当前存款为 50,和之前读到的 100 不相同,执行失败。

• 异常的过程:

存款 100。线程 1 获取到当前存款值为 100,期望更新为 50。线程 2 获取到当前存款为 100,期望更新为 50。线程 1 执行扣款成功,存款被改成 50。线程 2 阻塞等待中。在线程 2 执行之前,滑稽的朋友正好给滑稽转账 50,账户余额变成 100。轮到线程 2 执行了,发现当前存款为 100,和之前读到的 100 相同,再次执行扣款操作,这个时候,扣款操作被执行了两次。这就是是 ABA 问题搞的鬼。

2.5 解决方案:

给要修改的值,引入版本号(约定版本号只能加,不能减,每次操作一次余额,版本号都要 + 1)。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。CAS 操作在读取旧值的同时,也要读取版本号。真正修改的时候,如果当前版本号和读到的版本号相同,则修改数据,并把版本号 +1。如果当前版本号高于读到的版本号。就操作失败(认为数据已经被修改过了)。

可以看到:如果数据本身属于 ”能加也能减“,就容易出现 ABA 问题。

2.6 面试题:

1. 讲解下你自己理解的 CAS 机制:

全称 Compare and swap,即"比较并交换"。相当于通过⼀个原子的操作,同时完成 "读取内存,比较是否相等,修改内存" 这三个步骤。本质上需要 CPU 指令的支撑。

2. ABA问题怎么解决?

给要修改的数据引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。如果发现当前版本号和之前读到的版本号一致,就真正执行修改操作,并让版本号自增。如果发现当前版本号比之前读到的版本号大,就认为操作失败。

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

服务器神秘挂起:一场惊心动魄的内核探案

2024年6月17日&#xff0c;我们的运维团队突然收到了一连串的告警。监控大屏上&#xff0c;代表着不同 Sealos 可用区的绿点中&#xff0c;零星地闪烁起了一两个红点。 “奇怪&#xff0c;怎么有几台服务器突然 hang 住了&#xff1f;” 值班的小辉皱起了眉头。 这次故障的诡…

python遍历文件夹中所有图片

python遍历文件夹中的图片-CSDN博客 这个是之前的版本&#xff0c;现在这个版本会更好&#xff0c;直接进来就在列表中 path glob.glob("1/*.jpg")print(path)print(len(path))path_img glob.glob("1/*.jpg")path_img.extend(path)print(len(path_img))…

基于Hexo+GITHUB搭建个人博客网站(PS:不用域名,不用服务器,重点是免费,小白也能轻松掌握)

✌ 作者名字&#xff1a;高峰君主 &#x1f4eb; 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x1f4ac; 人生格言&#xff1a;没有我不会的语言&#xff0c;没有你过不去的坎儿。&#x1f4ac; &#x1f5…

25.模式和匹配

目录 一、概念二、模式的位置2.1 match分支2.2 if let表达式2.3 while let条件循环2.4 for循环2.5 let语句2.6 函数参数 三、模式是否会匹配失效四、模式语法4.1 匹配字面量4.2 匹配命名变量4.3 解构并分解值1&#xff09;解构结构体2&#xff09;解构枚举3&#xff09;解构嵌套…

动态规划数字三角形模型——AcWing 1015. 摘花生

动态规划数字三角形模型 定义 动态规划数字三角形模型是在一个三角形的数阵中&#xff0c;通过一定规则找到从顶部到底部的最优路径或最优值。 运用情况 通常用于解决具有递推关系、需要在不同路径中做出选择以达到最优结果的问题。比如计算最短路径、最大和等 注意事项 …

MySQL之复制(十一)

复制 复制的问题和解决方案 数据损坏或丢失的错误 当一个二进制日志损坏时&#xff0c;能恢复多少数据取决于损坏的类型&#xff0c;有几种比较常见的类型: 1.数据改变&#xff0c;但事件仍是有效的SQL 不幸的是&#xff0c;MySQL甚至无法察觉这种损坏。因此最好还是经常检查…

【小程序】聊天功能

文章目录 聊天功能实现功能实现思路后端前端效果展示 聊天功能 实现功能 要实现一个聊天机器人&#xff0c;它能够解答用户疑问&#xff0c;并且能够识别到用户聊天的主题&#xff0c;涉及到饮食方面时&#xff0c;会自动决定是否要去数据库中读取用户的相关喜好信息&#xf…

录音怎么转文字更高效?5款软件带你轻松拿捏文本转换~

临近大学生们最难熬的期末考试周&#xff0c;如何在短时间内复习完所有必考的科目也就成为大家迫在眉睫的首要任务。 想要在复习的过程中&#xff0c;更加高效地捕捉和整理关键信息、提高学习效率&#xff0c;那么录音转文字免费应用无疑是你的一大好帮手&#xff01; 倘若你…

YOLOv5改进 | SPPF | 具有多尺度带孔卷积层的ASPP【CVPR2018】

&#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a; 《YOLOv5入门 改进涨点》专栏介绍 & 专栏目录 |目前已有40篇内容&#xff0c;内含各种Head检测头、损失函数Loss、…

设计模式5-策略模式(Strategy)

设计模式5-策略模式 简介目的定义结构策略模式的结构要点 举例说明1. 策略接口2. 具体策略类3. 上下文类4. 客户端代码 策略模式的反例没有使用策略模式的代码 对比分析 简介 策略模式也是属于组件协作模式一种。现代软件专业分工之后的第一个结果是框架语音应用程序的划分。组…

WEB界面上使用ChatGPT

&#xff08;作者&#xff1a;陈玓玏&#xff09; 开源项目&#xff0c;欢迎star哦&#xff0c;https://github.com/tencentmusic/cube-studio 随着大模型不断发展&#xff0c;现在无论写代码&#xff0c;做设计&#xff0c;甚至老师备课、评卷都可以通过AI大模型来实现了&…

【数据结构与算法】动态查找表(二叉排序树,二叉平衡树)详解

二叉排序树的数据结构。 struct TreeNode {ElemType data;TreeNode *left, *right; }; using BiTree TreeNode *;结构体包含三个成员&#xff1a; data 是一个 ElemType 类型的变量&#xff0c;用于存储二叉搜索树节点的数据。left 是一个指向 TreeNode 类型的指针&#xff…

【Pandas驯化-16】一文搞懂Pandas中高性能query、eval函数技巧

【Pandas驯化-16】一文搞懂Pandas中高性能query、eval函数技巧 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 相关内容文档获取 微信公众…

Linux命令学习2

一.文件基础命令 1.alias-给某个命令取别名 使用方式&#xff1a;alias cl ls -la 说明&#xff1a;将ls -la命令取别名为cl,使用这种方式只是临时将命令取别名&#xff0c;重启中断后&#xff0c;就会失效。 问题1&#xff1a;如何永久性的设置命令的别名&#xff1f; 答…

生命在于学习——Python人工智能原理(4.3)

三、Python的数据类型 3.1 python的基本数据类型 3.1.4 布尔值&#xff08;bool&#xff09; 在Python中&#xff0c;布尔值是表示真或假的数据类型&#xff0c;有两个取值&#xff0c;True和False&#xff0c;布尔值常用于控制流程、条件判断和逻辑运算&#xff0c;本质上来…

ONLYOFFICE 桌面编辑器 8.1全新发布,更强大的编辑工具

ONLYOFFICE 8.1 一、什么是ONLYOFFICE&#xff1f;二、怎么安装 ONLYOFFICE 8.1三、主要功能介绍四、总结 一、什么是ONLYOFFICE&#xff1f; ONLYOFFICE 是一款功能强大的办公套件&#xff0c;旨在提供全面的文档、表格和演示文稿编辑解决方案。它集成了文字处理、电子表格和演…

基于 Native 技术加速 Spark 计算引擎

本文整理自 2024 年 6 月 DataFunSummit 2024 OLAP 架构峰会 Lakehouse 湖仓一体化架构论坛的同名主题分享。 今天分享的主题是基于 Native 技术加速 Spark 计算引擎&#xff0c;大家将会了解到如何基于 ClickHouse 来改造 Spark 引擎&#xff0c;最终获得较为可观的性能提升。…

正则表达式以及文本三剑客grep、sed、awk

正则表达式匹配的是文本内容&#xff0c;文本三剑客都是针对文本内容。 grep&#xff1a;过滤文本内容 sed&#xff1a;针对文本内容进行增删改查 awk&#xff1a;按行取列 一、grep grep的作用使用正则表达式来匹配文本内容 1、grep选项 -m&#xff1a;匹配几次之后停止…

第10章 启动过程组 (启动过程组的重点工作)

第10章 启动过程组 10.3启动过程组的重点工作&#xff0c;在第三版教材第362~364页&#xff1b; 文字图片音频方式 第一个知识点&#xff1a;项目启动会议 1、作用 标志着对项目经理责权的定义结果的正式公布&#xff0c;通常由项目经理负责组织和召开。2、目的 使项目各…

2024 cicsn 西南赛区 半决赛

文章目录 前言mcmf结构定义添加边遍历邻接点示例场景解决步骤1. 初始化2. 应用SPFA找最小费用增广路径 3. 增广操作4. 终止条件 结果分析 逆向maincaldeladdedit 思路expvlunexp qeme启动不行保护逆向 题目给的脚本模版 前言 不能联网搜是真坐牢 本来想等着全写了再发的&#…