JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

文章目录

  • 1、CAS的简介
    • 1.1、什么是CAS
    • 1.2、使用CAS的前后对比
    • 1.3、CAS如何做到不加锁的情况,保证数据的一致性
    • 1.4、什么是Unsafe类
    • 1.5、CAS方法参数详解
    • 1.6、CAS的原理
    • 1.7、 CAS的缺点
  • 2、原子操作类
    • 2.1、基本类型原子类
    • 2.2、数据类型原子类
    • 2.3、引用类型原子类
    • 2.4、对象的属性修改原子类
      • 2.4.1、它能帮我们解决什么问题
      • 2.4.2、使用要求
    • 2.5、原子操作增强类(jdk1.8才有)
  • 3、LongAdder效率这么快(源码分析篇)
    • 3.1、几个比较重要的成员变量以及方法
    • 3.2、LongAdder为什么这么快
    • 3.3、源码解析

1、CAS的简介

1.1、什么是CAS

CAS(Compare And Swap)的缩写,中文翻译成比较并交换,实现并发算法时常用到一种技术;他包含了3个操作数 ----- 内存位置,
,预期原值,更新值。执行CAS操作的时候,将内存位置的值与原值进行比较

  • 如果相匹配,那么处理器会自动将该位置的值更新为新值
  • 如果不匹配,处理不做任何操作,多个线程同时执行CAS操作,只有1个会成功

1.2、使用CAS的前后对比

没有CAS的时候, 我们利用sync和voliate保证符合操作的原子性
在这里插入图片描述
用了原子操作类后之后的操作,保证了i++的原子性,没有加入sync重量级别的锁
在这里插入图片描述

1.3、CAS如何做到不加锁的情况,保证数据的一致性

  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
  • 它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
  • CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,
  • 所以在多线程情况下性能会比较好。

1.4、什么是Unsafe类

  • Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于是一个后门,基于该类可以直接操作特定内存你的数据.Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接去操作内存,因为java中Cas操作的执行依赖于Unsafe方法
  • CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用诒范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

总结:CAS其实是Unsafe提供的一个方法,并且CAS是系统原语,本身就有执行过程不被中断的特性,天生就有保护原子性的特性

1.5、CAS方法参数详解

在这里插入图片描述

/**
var1: 表示要操作的对象
var2:要操作对象属性地址偏移量
var3:表示需要修改数据的期望值
var4:需要修改为的新值
**/
 boolean compareAndSwapObject(Object var1,long var2,Object var3,Object var4)

1.6、CAS的原理

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上) :

  • 1 AtomicInteger里面的value原始值为,即主内存中AtomiclInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 2线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
  • 3线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  • 4这时线租A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  • 5线程A重新获取valud值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

1.7、 CAS的缺点

  • 问题1 : 因为do While 循环,所以可能循环时间长,开销比较大
    在这里插入图片描述
    解释:因为cas是处于do while循环中的,如果一直没有修改成果,则CPU处于空转状态;开销比较大;

  • 问题2 :引出ABA问题

问题的产生

  • AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1
  • A线程把主内存的值1改为2,然后又该1,
  • 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过

上面过程A线程把数据从1->2->1 ,到了B线程读取的时候,进行比较比较他觉得这数据是没有人动过的,这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的

如何解决ABA问题

AtomicStampedReference[关心改了多少次,参考下一小节,原子类]

2、原子操作类

原子操作类,有很多这里主要把他们分类成如下几类 ,下面的代码示例中,会从中抽出来最经典的进行讲解

2.1、基本类型原子类

  • AtomicInteger (讲解案例)

public class TestMain {
    static AtomicInteger atomicInteger =   new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            },"t1").start();
        }
//          Thread.sleep(2000);
        System.out.println("获取到的值是:"+atomicInteger.get());
    }


    public static void add(){
        atomicInteger.getAndIncrement();
    }
}

虽然使用了atomicInteger,但是输出结果并不是10000;

原因: 还没有等t1 线程计算完成的时候,就已经主线程就已经获取结果了, 此时我们可以使用CountDownLatch ,让主线程等待子线程结束完毕,在运行

public class TestMain {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    static CountDownLatch countDownLatch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
                countDownLatch.countDown();
            }, "t1").start();
        }
//          Thread.sleep(2000);
        countDownLatch.await();
        System.out.println("获取到的值是:" + atomicInteger.get());
    }


    public static void add() {
        atomicInteger.getAndIncrement();
    }
}
  • AtomicBoolean
  • AtomicBoolean

2.2、数据类型原子类

  • AtomicIntegerArray(讲解案例)
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //对下标为0的位置 ,增加100
        atomicIntegerArray.addAndGet(0,100);
        //对下标为1的元素 ,加1
        atomicIntegerArray.getAndIncrement(1);
        System.out.println(atomicIntegerArray.get(0));
        System.out.println(atomicIntegerArray.get(1));

输出结果
100
1
  • AtomicLongArray
  • AtomicReferenceArray

2.3、引用类型原子类

  • AtomicReference
  • AtomicStampedReference[修改过几次,利用版本号的机制],参考ABA问题
  • AtomicMarkableReference[有没有修改过]
public class TestMain {

    static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100, false);

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"修改为1000,是否成功"+compareAndSet);
        },"t1").start();

        new Thread(()->{
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);
            boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName()+"修改为2000,是否成功"+compareAndSet);

        },"t2").start();
        Thread.sleep(2000);
        System.out.println("主线程获取的值为"+atomicMarkableReference.getReference());
    }

}

输出结果
t1获取的标志位为false
t2获取的标志位为false
t2修改为2000,是否成功true
t1修改为1000,是否成功false
主线程获取的值为2000

2.4、对象的属性修改原子类

2.4.1、它能帮我们解决什么问题

作用: 以一种线程安全的方式操作非线程安全对象内的某些字段
没有使用前遇到的问题

class Book {
    private Integer id;
    private String name;
    
    public synchronized void add(){
        id++;
    }
}

在之前我们只是想修改1个id值,却直接加了synchronized; synchronized锁的是一个对象? 有没有什么办法只锁Id呢?

2.4.2、使用要求

  • 更新的对象必须使用public volatile修饰

  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法NewUpdater()创建一个更新器,并且需要设置想要更新的类和属性

  • AtomicIntegerFieldUpdater

  • AtomicLongFieldUpdater

  • AtomicReferenceFieldUpdater

2.5、原子操作增强类(jdk1.8才有)

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator(提供了自定义的函数操作)
//这个0就是x,这个1就是y
        LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
        //0+1
        longAccumulator.accumulate(1);
        //1+2
        longAccumulator.accumulate(2);
        System.out.println(longAccumulator.get());

输出结果:3
  • LongAdder(只能用来计算加法,且之能从零开始计算)
LongAdder longAdder = new LongAdder();
        longAdder.increment();
        longAdder.increment();
        longAdder.increment();
        longAdder.increment();
        System.out.println(longAdder.sum());

输出结果 4

LongAdder和和AtomicInterget的性能对比效率对比

package com.tvu.interruput;

//需求:50个线程,每个线程100W次,总点赞数出来;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

class Reader {
    Long number = 0L;

    /**************sync的效率************************/
    public synchronized void syncAddClick() {
        number++;
    }

    public Long getSyncNumber() {
        return number;
    }

    /**************Atomic的效率************************/
    AtomicLong atomicLong = new AtomicLong();

    public void atomicAddClick() {
        atomicLong.getAndAdd(1);
    }

    public Long atomicGetClick() {
        return atomicLong.get();
    }

    /**************LongAdder的效率************************/
    LongAdder longAdder = new LongAdder();

    public void adderClick() {
        longAdder.increment();
    }

    public Long getAddNumber() {
        return longAdder.sum();
    }

    /**************LongAccumulator的效率************************/
    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);

    public void accMulatorClick() {
        longAccumulator.accumulate(1);
    }

    public long getAccNumber() {
        return longAccumulator.get();
    }

}

public class TestMain {

    public static final int _1W = 10000;
    public static final int threadNumber = 50;
    Reader reader = new Reader();
    static CountDownLatch syncCountDownLatch = new CountDownLatch(threadNumber);


    public static void main(String[] args) throws InterruptedException {
        Reader reader = new Reader();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < threadNumber; i++) {
            new Thread(() -> {
                for (int j = 0; j < _1W * 1000; j++) {
                    reader.accMulatorClick();
                }
                syncCountDownLatch.countDown();
            }, "t" + i).start();
        }
        syncCountDownLatch.await();
        long endTime = System.currentTimeMillis();


        //sync耗时时间为12395	 输出结果为500000000
        //System.out.println("sync耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getSyncNumber());

        //atomic耗时时间为7988	 输出结果为500000000
        //System.out.println("atomic耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.atomicGetClick());

        //AddLong耗时时间为599	 输出结果为500000000
        //System.out.println("AddLong耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAddNumber());

       // AccMulator耗时时间为642	 输出结果为500000000
        System.out.println("AccMulator耗时时间为" + (endTime - startTime)+"\t 输出结果为"+reader.getAccNumber());
    }
}



此时我们不禁好奇,为什么LongAdder效率这么快呢?(详情参考LongAdder源码解析)

3、LongAdder效率这么快(源码分析篇)

3.1、几个比较重要的成员变量以及方法

Striped64(他是LongAdder的父类),他主要包含了如下几个比较重要的属性和方法

  • Cell[] cells 数组,为2的幂,方便以后位运算
  • base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态 1:表示其他线程已经持有了锁
  • collide:表示扩容意向,false一定不会扩容,true可能会扩容。
  • casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
  • getProbe():获取当前线程的hash值
  • advanceProbe():重置当前线程的hash值

3.2、LongAdder为什么这么快

  • AtomicInteger 慢的原因: 当线程比较多的时候,利用cas,此时空转的线程就会增多,系统cpu就会有负担
  • 利用Cell[]数组分散热点,将value分散到不同的Cell数组中,不同线程会命中到不同的槽位中,各个线程只对自己的槽中那个值进行cas操作,这样热点就分散了,冲突的概率就减少许多了;如果想要获取真正的Long值,只要将各个槽的变量值累加返回即可

3.3、源码解析

在这里插入图片描述

第一次进longAccumulate: 数组的初始化

  1. 如果线程竞争不激烈,则直接在base基础上进行cas操作
    在这里插入图片描述
  2. 如果线程竞争不激烈,则直接在base基础上进行cas操作
  3. 初始化阶段,创建2个cell数组,进行赋值
    在这里插入图片描述

第二次进来,数组的赋值

  1. 确定槽位,进行cas赋值
    在这里插入图片描述
    2.这里是直接对目前的2个槽位进行赋值,没有进longAccumulate

第三次进来,进longAccumulate,需要根据Cell的状态进入不用的if代码块
在这里插入图片描述

进来的前提 : 目前的有了2个cell槽位,依旧竞争激烈
在这里插入图片描述

此时会根据不同的cell状态,进入到不同的代码分支逻辑模块; 我们先看Cell[]数组已经初始化的情况

状态1: 如果Cell[]数组已经初始化

  • 分支1 有槽位,但是还没有值,进行赋值
    在这里插入图片描述
  • 分支2 槽位cas修改失败,重新抢占
    在这里插入图片描述
    分支3: 有槽位,且有值,直接进行修改
    在这里插入图片描述
    分支4:如果槽位大于cpu的数量,则不扩容
    在这里插入图片描述分支5:新建一个cell数组,进行扩容,迁移
    在这里插入图片描述
    状态2:Cell[]数组未初始化[首次新建]
    在这里插入图片描述
    状态3:Cell[]数组正在初始化
    如果多个线程进行casCellsBusy 修改锁状态失败,则会进入到这个分支;cell初始化不了,就把值加给base进行累加
    在这里插入图片描述

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

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

相关文章

66-插入排序

目录 1.直接插入排序 2.折半插入排序 3.在数组arr[l...r]上使用插入排序 类似打扑克牌&#xff0c;整理牌的时候&#xff0c;都是把乱的牌向已经码好的牌中插入——天然的插入排序。 1.直接插入排序 每次选择无序区间的第一个元素&#xff0c;插入到有序区间的合适位置&am…

chatGPT中国入口-ChatGPT评论文章-ChatGPT怎么用

国内怎么玩chatGPT 如果您在国内使用ChatGPT&#xff0c;主要的问题可能是连接OpenAI服务器的速度和稳定性。由于OpenAI位于美国&#xff0c;可能受到中国的网络限制和防火墙的影响&#xff0c;造成访问速度比较慢或不稳定。为了解决这个问题&#xff0c;您可以采取以下方法&a…

idea常用快捷键,包的介绍,访问修饰符

这里有的是我自己定义的快捷键&#xff0c;可以到图片是指定位置查看对应的快捷键是什么。删除当前行&#xff0c;Ctrld复制当前行&#xff0c;自己配置CtrlShift向下箭头补全代码 alt /注释Ctrl /自动导入包在上面位置把两个选项选中&#xff0c;在要导入包的红色位置输入al…

(C++)模板分离编译面对的问题

什么是分离编译模板的分离编译什么是分离编译 一个程序&#xff08;项目&#xff09;由若干个源文件共同实现&#xff0c;而每个源文件单独编译生成目标文件&#xff0c;最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。 模板的分离编译 假如有以下…

Spring入门(万字详细附代码讲解)

1.Spring介绍 Spring其实就是一种开源框架,指的是Spring Framework,具有良好的生态,这也是Spring经久不衰的原因 用一句话概括,Spring就是一个集成了众多工具和方法的IOC容器 2.IOC容器 什么是IOC容器呢? IOC的中文翻译过来就是控制反转,IOC容器其实就是控制反转容器 那什…

2022蓝桥杯省赛——卡片

问题描述 小蓝有 k 种卡片, 一个班有 n 位同学, 小蓝给每位同学发了两张卡片, 一位同学的两张卡片可能是同一种, 也可能是不同种, 两张卡片没有顺序。没有两位同学的卡片都是一样的。 给定 n, 请问小蓝的卡片至少有多少种? 输入格式 输入一行包含一个正整数表示 n 。 输出…

Vue中的slot插槽

目录 &#xff08;一&#xff09;什么是slot插槽 (1)slot插槽的作用 (2)插槽的好处和使用场景 &#xff08;3&#xff09;slot插槽的分类 1、默认插槽 2、具名插槽 3、作用域插槽 &#xff08;一&#xff09;什么是slot插槽 (1)slot插槽的作用 slot具有“占坑”的作用…

Hadoop MapReduce各阶段执行过程以及Python代码实现简单的WordCount程序

视频资料&#xff1a;黑马程序员大数据Hadoop入门视频教程&#xff0c;适合零基础自学的大数据Hadoop教程 文章目录Map阶段执行过程Reduce阶段执行过程Python代码实现MapReduce的WordCount实例mapper.pyreducer.py在Hadoop HDFS文件系统中运行Map阶段执行过程 把输入目录下文件…

【GoF 23 概念理解】AOP面向切面编程

1. 什么是AOP——面向切面编程 AOP是一种编程范式&#xff0c;提供了一种从宁一个角度来考虑程序结构以完善面向对象编程&#xff08;OOP&#xff09; AOP是一个思想上的变化——主从换位&#xff0c;让原本主动调用的模块变成了被动等待&#xff0c;甚至在毫不知情的情况下被…

CodeTON Round 4 (Div. 1 + Div. 2, Rated, Prizes!)A~E

比赛连接&#xff1a;Dashboard - CodeTON Round 4 (Div. 1 Div. 2, Rated, Prizes!) - Codeforces A. Beautiful Sequence 题意&#xff1a; t(1≤t≤500)组测试每组给定大小为n(1≤n≤100) 的序列&#xff0c;判断它是否存在一个子序列是好序列。一个序列是好序列当且仅当至…

GPT-3:大语言模型小样本学习

论文标题&#xff1a;Language Models are Few-Shot Learners论文链接&#xff1a;https://arxiv.org/abs/2005.14165论文来源&#xff1a;OpenAI一、概述自然语言处理已经从学习特定任务的表示和设计特定任务的架构转变为使用任务无关的预训练和任务无关的架构。这种转变导致了…

Python - Huffman Tree 霍夫曼树实现与应用

目录 一.引言 二.Huffman Tree 理论 1.定义 2.结构 3.构造 三.Huffman Tree 实现 1.生成霍夫曼树 2.编码霍夫曼编码 3.解码霍夫曼编码 4.霍夫曼树编码解码实践 四.总结 一.引言 上篇 Word2vec 的文章中指出每次计算词库 N 个单词的 Softmax 计算量很大&#xff0c;…

办公工具-latex

一、排版总论 1.1 缺省权力 ​ 首先&#xff0c;最重要最需要强调的是&#xff0c;排版是一个信息量极大的工程。字体&#xff0c;格式&#xff0c;对齐方式&#xff0c;页眉页脚&#xff0c;都只是排版的冰山一角&#xff0c;可以说&#xff0c;一个人是没有办法完全控制一个…

JVM 运行时数据区概述及线程

当我们通过前面的&#xff1a;类的加载 --> 验证 --> 准备 --> 解析 --> 初始化&#xff0c;这几个阶段完成后&#xff0c;就会用到执行引擎对我们的类进行使用&#xff0c;同时执行引擎将会使用到我们运行时数据区。 运行时数据区结构 内存概念&#xff1a; 内存…

leetcode:只出现一次的数字 Ⅲ(详解)

前言&#xff1a;内容包括&#xff1a;题目&#xff0c;代码实现&#xff0c;大致思路&#xff0c;代码解读 题目&#xff1a; 给你一个整数数组 nums&#xff0c;其中恰好有两个元素只出现一次&#xff0c;其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任…

Qt界面编程(三)—— 父子关系、对象树、信号和槽(自定义信号和槽、Qt5与Qt4的写法)

一、Qt按钮小程序 1. 按钮的创建和父子关系 在Qt程序中&#xff0c;最常用的控件之一就是按钮了&#xff0c;首先我们来看下如何创建一个按钮&#xff1a; #include <QPushButton>QPushButton * btn new QPushButton; //设置父亲btn->setParent(this);//设置文字b…

接口测试-postman使用总结

一、为何使用postman postman是一款简单高效的接口测试工具&#xff0c;能够很方便发送接口请求&#xff0c;易于保存接口请求脚本&#xff0c;postman提供接口响应数据比对功能&#xff0c;可以设置预期结果作断言&#xff0c;还能把测试用例放在一个集合中批量执行&#xff…

【JavaWeb】9—监听器

⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ 如果可以&#xff0c;麻烦各位看官顺手点个star~&#x1f60a; 如果文章对你有所帮助&#xff0c;可以点赞&#x1f44d;…

torchvision.transforms 常用方法解析(含图例代码以及参数解释)

本文代码和图片完全源于 官方文档: TRANSFORMING AND AUGMENTING IMAGES 中的 Illustration of transforms&#xff0c;参数介绍源自函数对应的官方文档。 代码中的变换仅仅使用了最简单的参数&#xff1a;pad&#xff0c;size 等&#xff0c;这里展现的只是简单的变换&#xf…

C/C++每日一练(20230408)

目录 1. 删除无效的括号 &#x1f31f;&#x1f31f;&#x1f31f; 2. 合并K个升序链表 &#x1f31f;&#x1f31f;&#x1f31f; 3. 四数之和 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 …