Java锁策略-Java多线程(4)

news2024/11/16 22:41:32

(各位观众老爷下午好, 创作不易勒, 大家多多点赞收藏😘)

咱们废话不多讲, 下面细🔒


目录

前言

乐观锁  VS  悲观锁

        乐观锁

        悲观锁

        悲观乐观锁优缺点

轻量级锁  VS  重量级锁

        重量级锁

        轻量级锁

读写锁

自旋锁

互斥锁

可重入锁 VS 不可重入锁

死锁

 公平锁  VS  非公平锁

锁策略相关面试题

CAS

概念

        原理       

        实现自旋锁

        CAS的ABA问题

synchronized关键字



前言

         我们常见的锁策略不仅仅只是局限于Java这一门语言, 基本上任何和锁相关的, 都会涉及到锁策略, 之前所用的synchronized就是一种锁, 只不过它既是乐观锁也是悲观锁, 同时它还是一个互斥锁, 至于什么是乐观锁, 悲观锁, 我们下面将会进行讲解

乐观锁  VS  悲观锁

        那什么叫做乐观, 什么又叫做悲观呢? 所谓悲观, 就是总是考虑最坏的结果, 而乐观则是等到事情快要到的时候才去做.

        举个例子: 例如有两个同学A和B, 他们两个去问老师问题, 对于A, A提前预测了老师可能有事情(悲观), 所以A同学就提前给老师发送了个消息 ,问老师在忙吗,下午我可以问一个问题吗, 如果老师回复了可以, 则A同学就可以直接去问问题, 如果不可以则A同学就需要等待, 这个就是悲观锁.
        对比于A同学, B同学就直接认为, 老师是比较闲的, 就直接去问问题(没有加锁, 直接访问资源), 如果老师确实比较忙, 那么B也就不会去打扰老师, 就下次再来, 如果老师真闲着, 那么问题就直接解决了, 这就是乐观锁

        乐观锁

        假设数据一般情况下不会产生并发冲突, 所以就在数据进行提交更新的时候, 才会正式对数据是否产生冲突进行检测, 如果产生并发冲突了, 则返回用户的错误信息, 让用户自己去解决

        悲观锁

        总是假设最坏的情况发生, 每次拿数据的时候都认为别人会修改, 所以就在拿数据的时候上锁, 这样子别人想拿到这个数据就需要阻塞等待.

        悲观乐观锁优缺点

        我们知道, 加锁操作是会影响性能的, 对于锁来说, 我们称之为, 非必要不加锁, 所以在一般情况下, 悲观锁是提前就认为在读取的过程中都认为别人会修改这个数据, 所以就给拿的时候上了锁, 这个时候对比于乐观锁来说, 悲观锁的加锁操作就会相较于频繁一些, 所以通常认为悲观锁的效率会更低一点, 乐观锁的效率会更高一点(但也不是绝对的)

        synchronized就是默认乐观锁的策略, 当发现锁竞争比较频繁的时候就会自动切换为悲观锁.

轻量级锁  VS  重量级锁

        简单来说就是, 轻量级加锁解锁操作过程更高效, 重量级加锁解锁过程更慢,更低效. 这种结论可能与前面的乐观锁, 悲观锁类似, 在对比区分上有一定的重合.

        锁的核心特性"原子性", 这种机制的出现可以追踪到CPU这样的硬件设备身上, 也就是说:

  • CPU提供了"原子性操作"的指令
  • 操作系统基于CPU的原子性操作指令, 实现了mutex互斥锁
  • 然后JVM基于这个操作系统提供的mutex互斥锁, 实现了synchronizedReentrantLock等关键字和类(此处的synchronized并不仅仅是对mutex进行封装, 在synchronized里面还实现了许多其他的功能)

        重量级锁

        重量级锁加锁机制重度依赖于操作系统提供的mutex互斥锁, 这也就意味着重量级锁将会涉及到大量的用户态和内核态的切换, 也就是说明将会消耗更多的资源

        轻量级锁

        轻量级锁加锁机制上尽可能的不使用mutex, 而是尽量在用户态完成, 随后才可能在没有完成的基础上使用mutex, 仅存在少量的用户态和内核态的切换

重温一下什么是用户态, 什么是内核态: 
        内核态:  处于内核态CPU可以访问任何数据, 包括io设备, 如硬盘等,  处于内核态的CPU可以从一个程序切换到另外一个程序, 并且占用CPU不会发生抢占情况.
        用户态: 处于用户态的CPU只能受限的访问内存, 并且不允许访问io设备, 用户台下的CPU不允许独占, 也就是CPU能够被其他程序获取到

        总体来说, 内核态运行的时候比用户态安全.用户态的时间成本是比较可控的. 内核态的时间成本是不太可控的.  内核态这时效率是很低的.

        所以我们这里的synchronized开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

读写锁

        在我们之前谈到的原子性和可读性问题上就说明过, 多线程之间读取方之间不会昌盛线程安全问题, 但是如果数据写入方和读取方之间需要进行互斥, 我们所说, 非必要不加锁,  于是就设计了读写锁这个操作, 如果在只有读取操作这样的线程安全的代码中, 给读取操作加锁, 这样就会产生极大的性能消耗, 所以为了更加精细化锁的操作, 也就出现了读写锁.

        读写锁, 在加锁时需要额外表名读写意图, 例如如果只是单纯的读取方之间, 就不需要互斥, 如果存在写入的操作, 那么必须要求任何人都需要互斥.

        下面详细讲讲读取数据写入数据的线程安全问题:

  1. 两个或者多个线程, 都只是去读取一个数据, 此时没有线程安全问题, 直接并发读取即可
  2. 两个线程写一个数据, 会有线程安全问题,
  3. 多个线程读和写, 存在线程安全问题, 也就是之前所说的load, op, save的原子性问题等

接下来的读写操作就是把读操作和写操作分开来加锁, 

Java标准库提供了ReentrantReadWriteLock类, 实现了读写锁, 底下有ReadLock和WriteLock类表示读加锁或者写加锁:

  • ReenTrantReadWriteLock.ReadLock类, 提供 lock() / unlock()方法来进行加 / 解 锁
  • ReenTrantReadWriteLock.WriteLock类, 提供lock() / unlock() 方法来进行加 / 解 锁

其中会不会引起线程阻塞等待, 取决于:

  • 读加锁与读加锁之间不互斥
  • 读加锁与写加锁之间互斥
  • 写加锁和写加锁之间互斥

适应:

        读加锁适应于频繁度, 少量写的场景中, 比如我们日常生活中的点名, 班级学生表之类的(synchronized不是读写锁)

自旋锁

        线程在抢锁失败后进入阻塞状态, 放弃CPU, 需要经过很长时间后, 才有可能被系统调度, 但是在实际情况下, 其实抢锁失败, 过不了多久锁就后悔被重新释放, 这个时候就没有必要放弃CPU, 这个时候可以使用自旋锁来处理这样的问题:

    自旋锁伪代码:

while (抢锁(lock) == false) {
    // 代码
}

解释: 如果获取锁失败, 就会立即尝试再次获取锁, 无限循环, 知道拿到锁为止.第一次获取所失败, 第二次尝试就会在极短的时间内执行, 一旦锁被释放, 第一时间就能拿到锁.

自旋锁是轻量级线程的一种典型实现
挂起等待锁是重量级锁的一种典型实现

优缺点: 自旋锁没有放弃CPU, 不涉及线程阻塞等待和调度, 一点所被释放就能第一时间拿到锁, 但是如果锁被其他线程长时间占用, 那么这个线程就会持续消耗CPU资源(而挂起等待是不消耗CPU的)

互斥锁

        例如我们经常使用的synchronized, 就是一种互斥锁, 他就只是单纯的给指定代码块加锁:

synchronized(locker) {
    // code
}

他只有两个操作, 一个是进入代码块, 开始加锁, 一个是出了代码块就释放锁. 其他线程想要拿到锁就必须阻塞等待

可重入锁 VS 不可重入锁

        可重入锁字面上也就是"可重新进入的锁", 即允许同一个线程多次获取同一把锁. 

举个例子: 一个递归函数里面有加锁操作, 但是可以思考一个问题, 如果在递归的过程中这个函数已经拿到锁了, 那么他的递归函数会阻塞自己吗? 

        如果会. 那么这个锁就是不可重入锁, 如果不会阻塞, 那么叫可重入锁.

对于Java中, 以Reentrant开头命名的锁都是可重入锁, 而且JDK都提供的所有的现有的Lock实现类, 包括synchronized 等都是可重入的.

如何把自己锁死?

例如:

symchronized pubilc static void Function(int n) {
    if ( n == 1 || n == 2) {
        return 1;
    }
    return Function(n - 1)  + Function(n - 2);
}

调用这个函数, 如果他是不可重入的, 那么就会在第二次调用这个Funciton方法的时候进入阻塞等待. 但是 第一个Function拿到的锁必须要等到后面的递归结束之后才能释放锁, 而后面的递归又在阻塞等待. 这个时候就会死锁

死锁

        关于死锁的两个情况, 也就是:

  1. 一个线程一把锁, 但是一个线程已经占有一个锁, 但是这个线程再次对这个锁进行获取:
    synchronized(locker) {
        synchronized(locker) {
            // code
        }
    }

            第一次这个线程获取到了这个锁, 第二次继续尝试索取这个锁, 但是需要等待当前这个锁释放, 当前锁释放了才有可能, 但是当前代码在没有获取到锁并且走出代码块之前是无法释放锁的, 在逻辑上就矛盾了.
            我们日常开发中经常遇到这种情况, 例如:

    class BlockingQueueMy {
        synchronized void fun() {
            int tem = this.size();
            // ....
        }
        
        synchronized int size() {
            // code....
        }
    }

    我们对方法加锁, 那么这个锁的对象都是调用这个方法的实例, 就形成了上述的死锁的情况.. 但是在实际开发过程中并不会经常死锁, 就比如synchronized这个锁是一个可重入锁, 在重复加锁的过程中使回去判断一个当前获取锁的线程是否已经是锁的拥有者了.

  2. 两个线程两把锁, 即使是可重入锁, 也会出现死锁的情况

    例如有如下代码案例:

             当一个线程获取了locker1, 另外一个线程获取了locker2之后, 然后线程1想继续获取lokcer2, 同时线程2想继续获取locker1, 这个时候就产生了冲突 如果线程1想要获取locker2, 就必须等待线程2释放掉lokcer2, 但是locker2释放的条件是线程2执行出代码块, 但是在出代码快之前必须得先获取locker1, 但是要获取locker1就必须等待locker1被线程1释放, 想要线程1被释放就又得等待线程2释放locker2, 这就形成了死锁, 就算是可重入的, 也会被死锁
  3. N个线程, M把锁.
          假设这里有五个线程和五把锁, 每个锁进行随机的获取锁和释放锁, 如果他们想要的锁已经被占有, 那么他们就会进行阻塞等待, 等待的过程中不会释放手中已经拿到的锁. 并且线程需要拿到最近的两个锁才能执行任务, 
          如图:
    假设, 线程1拿到locker1, 线程2拿到locker2, ...一直到线程5拿到locker5, 此时, 每一个线程都拿到了最近的一个锁, 但是还有另外一个锁没有拿到, 例如, 当前线程5去拿锁1想要完成任务, 但是locker1被线程1占有, 此时就会阻塞等待, 那么对于其他的线程同样如此, 就会陷入死锁的情况

解决上述死锁的策略:

  1. 对于同一个线程重复获取同一把锁, 我们可以采取将锁设为可重入锁, 就可以避免这个问题
  2. 对锁进行编号, 约定加锁顺序, 例如, 对于例子3来说, 我们可以让线程1先获取locker1和locker2,线程2获取locker3和locker2, 但是locker已经被线程1占用, 此时就进入阻塞等待, 线程3和线程4,5同样如此, 此后等线程1释放锁, 然后线程2就可以拿到locker2, 以此类推, 就可以解决重复等待的条件
  3. 两个线程两把锁的, 我们可以约定加锁顺序, 例如:
    线程都遵守这个顺序来加锁, 就不会进行循环等待了.

 公平锁  VS  非公平锁

        一个锁, n个线程, 每个线程拿到锁的可能是等概率的, 或者说是随机拿到锁的, 这种情况就称为非公平锁, synchronized就是一个非公平锁.

        相反, 如果一个锁, 线程占用这个锁都是按先来后到或者是其他顺序来的, 则称为公平锁.

锁策略相关面试题

CAS

        概念

        什么是CAS? CAS全程为Compare and swap, 英文翻译也就是"比较并交换", 一个CAS涉及到一下操作:
        我们假设主存或者是寄存器中存储着一个值为int a = 0; 我们在使用CAS的时候首先将其读取到工作内存,  然后将其修改然后存储回寄存器或者主存当中:

我们假设内存中的原数据 V ,旧的预期值 A ,需要修改的新值 B
1. 比较 A V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V 。(交换)
3. 返回操作是否成功。

        其本质上CAS是一条CPU指令
       

        原理       

 例如加下来的代码案例: 我们创建两个线程, 对同一个线程进行自增操作:

public static void main(String[] args) throws InterruptedException {
        final int[] a = {0};
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 50000; i++) {
                    a[0]++;
                }
            }
        });
        Thread t2 = new Thread(()-> {
            for(int i = 0; i < 50000; i++) {
                a[0]++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(a[0]);

    }

他的输出值时随机的, 因为线程的随机调度, 导致他们存在脏读或者是脏数据问题, 也就不能正确的得到想要的结果. 也就是存在++操作的原子性问题.

基于CAS, Java标准库提供了AtomInteger等类, 来保证前后置++和--的原子性问题

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // num++
                num.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // get 获取到数值
        System.out.println(num.get());
    }

 其中: incrementAndGet是前置++, decrementAndGet是后置--....

 也就是, 读取完数据之后, 回去检查工作内存中的数据和寄存器当中的数据是否是一致的, 如果一致,就将swapData和寄存器中的0交换, 完成自增操作. 

但是如果是下面这种情况:

 数据比较时, 发现 0 != 1, 结果返回false, 也就是操作失败.

其伪码如下:

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

利用这种机制, 及时感知到了另外一个线程修改了这个数据, 就相当于提前判断了寄存器当中的值是否被修改. 也就保证了原子性或者说是内存可见性.

        实现自旋锁

public class SpinLock {
    private Thread owner = null;
    
    public void lock() {
        while (!CAS(this.owner,null,Thread.currentThread())) {}
    }

    public void unlock(){
        this.owner = null;
    }
}

通过循环查看这个锁是否被占有, 如果占有就自选等待, 如果没有被线程持有之后, 那么就把owner设为当前尝试加锁的线程, 然后判断CAS为false之后, 跳出循环.

        CAS的ABA问题

        我们说, CAS可以提前检测寄存器内容是否被修改, 但是, 如果遇到像这种修改前和修改后值相等的该如何判断?

        解决方案, 我们不用去判断这个值是否相等, 我们去设置一个版本号, 每次的修改都有一个对应的版本号, 比较的时候就去比较版本号, 如果版本号如果不一样, 就返回false.

synchronized关键字

        synchronized关键字属于一个具有锁升级策略的锁, 这个过程具有不可逆性.

        对于一个拿到synchronized锁的线程, 首先是这个线程针对锁做一个标记,如果在代码执行的过程中, 都没有别的线程去竞争这个锁, 那么就不需要加锁, 这也是非必要不加锁的特点. 此时synchronized被称为偏向锁. 这种标记非常轻量, 既保证了效率, 与保证了线程安全.

        当有线程来竞争之后就变成了轻量级锁, 也是一种自旋锁, 遇到锁竞争时会进行自选等待, 如果锁竞争更加激烈之后, 就变成了重量级锁. 假设10个线程竞争一个锁, 那么有9个在阻塞等待, 那么这么多线程自选, CPU的消耗就会非常大, 既然如此就升级为重量级锁, 在内核阻塞等待, 资源消耗少儿, 但是这就意味着放弃CPU, 有内核进行随机调度, 效率也会慢很多. 不能保证释放锁的第一时间有线程拿到锁.

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

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

相关文章

IPSCE

文章目录 1.什么是数据认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段?2.什么是身份认证&#xff0c;有什么作用&#xff0c;有哪些实现的技术手段?3.什么VPN技术?4.VPN技术有哪些分类?5.IPSEC技术能够提供哪些安全服务?6.IPSEC的技术架构是什么?7.AH与ESP…

今天面了个字阿里拿38K出来的,真是纹身师闭眼,秀了我一脸啊

公司前段缺人&#xff0c;也面了不少测试&#xff0c;前面一开始瞄准的就是中级的水准&#xff0c;也没指望来大牛&#xff0c;提供的薪资在15-20k&#xff0c;面试的人很多&#xff0c;但平均水平很让人失望。看简历很多都是4年工作经验&#xff0c;但面试中&#xff0c;不提测…

8.java程序员必知必会类库之嵌入式SQL数据库

前言 嵌入式内存数据库&#xff0c;作为嵌入到应用内部的数据库&#xff0c;在正常生产业务流程中使用不多。现在一般公司通用架构都是应用和数据分离&#xff0c;解耦数据和应用。但是&#xff0c;在某些特殊场景中&#xff0c;这种嵌入式数据库是比较好的选择。 在某些单元…

如何在开发阶段保证软件工程质量 (程序员要做些什么)

前言 大家常说&#xff1a;“代码和人&#xff0c;有一个能跑就行”&#xff0c;但这并不意味着我们可以放弃职业道德。与土木工程一样&#xff0c;软件工程也需要一些可度量的指标来衡量产品的交付质量。一个高质量的软件绝对不能只靠测试人员来保证&#xff0c;更不能相信程…

在SaleSmartly(ss客服)中为Messenger 提供无缝支持体验

客户希望您在他们所在的地方与他们见面&#xff0c;这意味着打开多个沟通渠道。但是&#xff0c;当您通过电子邮件、实时聊天、社交等方式进行通信时&#xff0c;对话很容易丢失、被忽视和杂乱无章。 而Messenger的受欢迎程度&#xff0c;以及Meta的无所不在&#xff0c;使Face…

每日学术速递4.18

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.Inpaint Anything: Segment Anything Meets Image Inpainting 标题&#xff1a;Inpaint Anything&#xff1a;分割任何东西满足图像修复 作者&#xff1a;Tao Yu, Runseng Feng, R…

企业用户如何选择合适的服务器配置方法教程

随着互联网信息的飞速发展&#xff0c;任何企业都脱离不了互联网&#xff0c;越来越多的企业都通过互联网实施无纸化的办公&#xff0c;互联网推广一体化整体型推广、互联网电子商务。中小型企业网站如何选购云服务器配置呢&#xff1f;但是&#xff0c;实现这些的最最基础的条…

ai改写句子软件-ai改写

AI免费伪原创&#xff1a;助力网站内容升级 您是否曾经为网站优化而烦恼&#xff0c;无论是内容更新还是SEO优化&#xff0c;都需要大量的时间和精力。但是&#xff0c;您是否知道&#xff0c;现在有一款能够使用AI技术来帮助您完成这些任务&#xff0c;而且还是免费的呢&…

【Git 学习】

Git 学习 一、Git的使用1. Git下载安装2. Git 命令3. Git推送代码步骤4. Git基本工作流程5. Git历史版本切换6. Git分支管理6.1 创建新分支6.2 切换分支6.3 合并分支6.4 删除分支 7. 远程仓库的工作流程7.1 具体流程 8.推送到远程仓库9. 代码冲突问题10. IDEA 集成Git10.1 版本…

说说webpack的构建流程?

① 初始化流程 从配置文件和 Shell 语句中读取与合并参数&#xff0c;并初始化需要使用的插件和配置插件等执行环境所需要的参数。 配置文件默认下为 webpack.config.js&#xff0c;也可以通过命令的形式指定配置文件&#xff1b; 主要作用是用于激活webpack的加载项和插件&am…

手写axios源码系列一:axios核心知识点

文章目录 axios的核心功能1、axios 函数对象2、dispatchRequest 发送请求3、interceptors 拦截器4、cancelToken 取消请求 最近从头搭建了一个vue小项目&#xff0c;想使用 axios 作为请求接口的第三方库。结果使用了 axios 这么长时间&#xff0c;想封装一下 axios &#xff0…

Nacos2.2.2开启鉴权配置

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、更改application.properties中的配置二、修改配置 前言 最近公司开启了一个新的电商项目&#xff0c;项目中用到了Naocs作为注册中心和配置中心&#xff0…

WindowsUbuntu下python程序打包

Python程序的运行必须要有Python的环境&#xff0c;但是程序编出来是用的&#xff0c;如果是给别人用&#xff0c;而他/她的电脑上又没有Python程序运行的环境怎么办呢&#xff1f;总不能让他/她去安装一个吧&#xff1f;这时我们就要将Python程序打包为exe可执行文件&#xff…

【文章学习系列之模型】PatchTST

本章内容 文章概况模型结构实验结果长期预测表征学习 消融实验分块和通道独立性不同的回顾窗口 总结 文章概况 《A Time Series is Worth 64 Words: Long-term Forecasting with Transformers》是2023年发表于ICLR的一篇文章。该文章借鉴了计算机视觉领域的Vision Transformer…

树形DP分析

树形dp 简单来说树形 d p 就是在树上做 d p 罢了 简单来说树形dp就是在树上做dp罢了 简单来说树形dp就是在树上做dp罢了 树嘛&#xff0c;就要符合除了根节点外每个节点只有一个父节点 树嘛&#xff0c;就要符合除了根节点外每个节点只有一个父节点 树嘛&#xff0c;就要符合除…

# 从车灯模组的角度聊聊信息安全需求

文章目录 1. 前言2.信息安全需求2.1 硬件安全2.1.1 接口安全2.1.2 主板安全2.1.3 芯片安全 2.3 系统安全2.3.1 代码安全2.3.2 软件读保护2.3.3 安全启动2.3.4 安全升级2.3.5 安全诊断 2.4 通信安全2.5 数据安全 3. 安全启动流程3.1 基于签名技术的安全启动方案3.2 基于对称签名…

netty源码阅读--处理客户端请求

背景 netty是一个非常成熟的NIO框架&#xff0c;众多apache的顶级项目底层通信框架都是用的是netty,本系列博客主要是记录自己复习netty源码的过程&#xff0c;重在理解netty的关键如&#xff1a;如何启动&#xff0c;如何接受网络数据、netty的内存管理机制以及编解码器等&am…

Python学习笔记--面向对象

未完待续。。。。。 &#xff08;一&#xff09;面向对象的基本概念 我们之前学习的编程方式就是面向过程的 面相过程和面相对象&#xff0c;是两种不同的编程方式 对比面向过程的特点&#xff0c;可以更好地了解什么是面向对象 1.1过程和函数 过程是早期的一个编程概念 过程…

4.3 分段低次插值

学习目标&#xff1a; 如果我要学习分段低次插值&#xff0c;我可能会采取以下几个步骤&#xff1a; 学习插值的基本概念和方法 在学习分段低次插值之前&#xff0c;我需要先掌握插值的基本概念和方法&#xff0c;例如拉格朗日插值、牛顿插值和内维尔方法等。这些基础知识可…

C#调用C++封装的SDK库(dll动态库)——上

C#调用C封装的SDK库(dll动态库)——上 一、C封装库 通过前几篇文章&#xff0c;我们封装了C的动态DLL库&#xff0c;有Qt版的&#xff0c;有C版的&#xff0c;当然还有介绍了Pimpl模式在SDK封装中的使用&#xff1a; Qt创建SDK VS创建SDK Pimple在SDK封装中的应用 但是&a…