【Java 并发编程】一文详解 Java 内置锁 synchronized

news2024/10/6 5:55:40

一文详解 Java 内置锁 synchronized

  • 1. 前言
    • 1.1 并发编程中存在线程安全问题
    • 1.2 设计同步器的意义
    • 1.3 如何解决线程并发安全问题?
  • 2. synchronized 的用法
    • 2.1 修饰实例方法,锁是当前实例对象
    • 2.2 修饰静态方法,锁是当前类对象
    • 2.3 修饰代码块,锁是括号里面的对象
    • 2.4 总结
  • 3. synchronized 的实现原理
    • 3.1 synchronized 是怎么加锁的?
      • (1)synchronized 修饰代码块时
      • (2)synchronized 修饰实例方法
    • 3.2 synchronized 同步概念
      • (1)Java 对象头
      • (2)监视器锁(Monitor)
    • 3.3 synchronized 的特性
      • 3.3.1 原子性
      • 3.3.2 可见性
      • 3.3.3 有序性
      • 3.3.4 可重入
      • 3.3.5 非公平
  • 4. 锁优化
    • 4.1 无锁
    • 4.2 偏向锁
    • 4.3 轻量级锁
    • 4.4 重量级锁
    • 4.5 锁升级全过程

1. 前言

1.1 并发编程中存在线程安全问题

  1. 存在共享数据;

  2. 多线程共同操作共享数。

关键字 synchronized 可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时 synchronized 可以保证一个线程的变化可见(可见性),即可以代替 volatile。

1.2 设计同步器的意义

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之为临界资源。这种资源可能是:对象、变量、文件等。

  • 共享:资源可以由多个线程同时访问;

  • 可变:资源可以在其生命周期内被修改。

引出的问题:由于线程执行的过程是不可控的,所以需要采用同步机制来协调对对象可变状态的访问。

1.3 如何解决线程并发安全问题?

实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

Java 中,提供了两种方式来实现同步互斥访问:synchronizedLock

同步器的本质就是加锁。加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)。

不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,并不具有共享性,不会导致线程安全问题。

2. synchronized 的用法

synchronized 内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

2.1 修饰实例方法,锁是当前实例对象

public class Juc_LockOnThisObject {

    private Integer stock = 10;

    public synchronized void decrStock() {
        --stock;
        
        System.out.println(ClassLayout.parseInstance(this).toPrintable());
    }
}

synchronized 修饰非静态方法锁定的是该类的实例,同一实例在多线程中调用才会触发同步锁定 所以多个被 synchronized 修饰的非静态方法在同一实例下只能多线程同时调用一个。

2.2 修饰静态方法,锁是当前类对象

public class Juc_LockOnClass {

    private static int stock;
    
    public static synchronized void decrStock(){
        System.out.println(--stock);
    }
}

synchronized 修饰静态方法锁定的是类本身,而不是实例,会作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

2.3 修饰代码块,锁是括号里面的对象

public class Juc_LockOnObject {

    public static Object object = new Object();

    private Integer stock = 10;

    public void decrStock() {
        // T1,T2
        synchronized (object) {
            --stock;
            if (stock <= 0) {
                System.out.println("库存售罄");
                return;
            }
        }
    }
}

synchronized 指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得当前 class 的锁。

2.4 总结

  • synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁。

  • synchronized 关键字加到实例方法上是给对象实例上锁。

一起看一个 synchronized 使用经典实例—— 线程安全的单例模式:

public class LazySingleton {
    /**
     * 提供一个私有的静态属性,对于 volatile 修饰的字段,可以防止指令重排
     */
    private volatile static LazySingleton instance = null;

    /**
     * 提供一个私有构造函数:避免类在外部被实例化
     */
    private LazySingleton() {

    }

    public static LazySingleton getInstance() {
        // 先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    // 类对象加锁
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

3. synchronized 的实现原理

3.1 synchronized 是怎么加锁的?

synchronized 是基于 JVM内置锁 实现,通过内部对象 Monitor(监视器锁) 实现,基于进入与退出 Monitor 对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的 Mutex lock(互斥锁) 实现,它是一个重量级锁,性能较低。当然,JVM 内置锁在1.5之后版本做了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Eliminaction)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与 Lock 持平。

(1)synchronized 修饰代码块时

/**
 * @author pointer
 * @date 2023-04-21 21:16:47
 */
public class SynchronizedDemo {

    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class

在这里插入图片描述
synchronized 修饰代码块时,JVM 采用 monitorentermonitorexit 两个指令来实现同步,其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

(2)synchronized 修饰实例方法

/**
 * @author pointer
 * @date 2023-04-21 21:16:47
 */
public class SynchronizedDemo {

    public synchronized void method() {
        System.out.println("synchronized 实例方法");
    }
}

反编译一下:

在这里插入图片描述

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

3.2 synchronized 同步概念

monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是基于 Monitor(监视器锁) 实现的。

实例对象结构里有对象头,对象头里面有一块结构叫 Mark Word,Mark Word 指针指向了 Monitor。

(1)Java 对象头

在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding):

在这里插入图片描述

synchronized 是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在 Java对象头 里的,Java对象头是什么呢?

我们以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针):

Mark Word: Mark Word存储对象自身的运行数据,如哈希码、GC分代年龄、锁状态标志、偏向时间戳(Epoch) 等信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。

Klass Point: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

(2)监视器锁(Monitor)

监视器锁(Monitor) 本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

  • mutex的工作方式:

在这里插入图片描述

  1. 申请 mutex;
  2. 如果成功,则持有该mutex;
  3. 如果失败,则进行spin自旋。spin的过程就是在线等待mutex,不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止;
  4. 依据工作模式的不同选择yiled还是sleep;
  5. 若达到sleep限制或者被主动唤醒或者完成yield,则重复1-4 步,直到获得为止。

由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中;

synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

3.3 synchronized 的特性

3.3.1 原子性

原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。被 synchronized 修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。

3.3.2 可见性

可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的synchronizedvolatile 都具有可见性。

synchronized怎么保证可见性?

  • 线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值;

  • 线程加锁后,其它线程无法获取主内存中的共享变量;

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。

3.3.3 有序性

有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。

synchronized怎么保证有序性?

  • synchronized 同步的代码块,具有排他性,一次只能被一个线程拥有,所以 synchronized 保证同一时刻,代码是单线程执行的;

  • 因为 as-if-serial 语义的存在,单线程的程序能保证最终结果是有序的,但是不保证不会指令重排;

  • 所以 synchronized 保证的有序是执行结果的有序性,而不是防止指令重排的有序性。

3.3.4 可重入

可重入特性指的是同一个线程获得锁之后,可以再次获取该锁(一个线程可以多次执行 synchronized,重复获取同一把锁)。

public class NewThread {

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (NewThread.class) {
                System.out.println("我是 run");
            }

            test01();
        }

        void test01(){
            synchronized (NewThread.class) {
                System.out.println("我是 test01");
            }
        }
    }

    public static void main(String[] args) {
        MyThread myThread01 = new MyThread();
        MyThread myThread02 = new MyThread();

        myThread01.start();
        myThread02.start();
    }
}
  • 原理

synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在执行完一个同步代码块时,计数器数量就会减 1,直到计数器的数量为 0 才释放这个锁。

  • 优点

可以避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入)。

3.3.5 非公平

非公平是指多个线程在抢占锁时JVM并不会保证线程先来后到的顺序,非公平性可以提升吞吐量,因为少了维护线程顺序的开销。

4. 锁优化

synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 synchronized 效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态锁可以升级但不能降级

在这里插入图片描述

先看一下四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:

锁状态存储内容存储内容
无锁对象的hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10

4.1 无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

4.2 偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

4.3 轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

4.4 重量级锁

升级为重量级锁时,锁标志的状态值变为 10,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

4.5 锁升级全过程

在这里插入图片描述
参考文章:
https://zhuanlan.zhihu.com/p/29866981 - Java synchronized原理总结
https://www.cnblogs.com/xfeiyun/p/15871647.html#gallery-6 - Java内置同步锁synchronized深入理解
https://www.cnblogs.com/three-fighter/p/14396208.html - synchronized详解

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

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

相关文章

简单创建SSM项目

1、在idea中创建项目 点击new-project&#xff0c;点击Maven项目&#xff0c;勾选 creat from archetype &#xff0c;找到maven-archetype-webapp 写好相关信息 点击下一步&#xff0c;需要检查maven环境 点击后下载对应的插件&#xff0c;选择项目地址。 开始下载资源&#x…

rust vscode编辑器常用插件与配置

插件&#xff1a;rust-analyzer 会实时编译和分析你的 Rust 代码&#xff0c;提示代码中的错误&#xff0c;并对类型进行标注 插件的完整手册地址&#xff1a;https://rust-analyzer.github.io/manual.html。 插件&#xff1a; rust syntax 为代码提供语法高亮。 …

就挺无语的,这是有脾气的博客

文章目录 前言1. 背景2. 使用3. 公众号体验4. 结束语 前言 ChatGPT已经推出两个多月了&#xff0c;热度已经不减。ChatGPT由人工智能研究实验室OpenAI在2022年11月30日发布的全新聊天机器人模型&#xff0c;一款人工智能技术驱动的自然语言处理工具。它能够通过学习和理解人类的…

大厂都用DevOps!十分钟带你了解自动化在DevOps中的运用

Hi&#xff0c;大家好。DevOps、CI/CD、Docker、Kubernetes……好像全世界都在谈论这些技术&#xff0c;以至于你觉得即将到达NoOps阶段。别担心&#xff0c;在工具和各种最佳实践的浩瀚海洋中感到迷失是正常的&#xff0c;是时候让我们来分析一下DevOps到底是什么了。 一、De…

JAVA类加载机制

在Java的世界里&#xff0c;每一个类或者接口&#xff0c;在经历编译器后&#xff0c;都会生成一个个.class文件。 类加载机制指的是将这些.class文件中的二进制数据读入到内存中&#xff0c;并对数据进行校验&#xff0c;解析和初始化。最终&#xff0c;每一个类都会在方法区保…

ChatGPT - 学习和提高新技能的Prompt

文章目录 Prompt例子 Prompt “我想学习/提高[技能]。我是一个完全的初学者。创建一个30天的学习计划&#xff0c;可以帮助像我这样的初学者学习和提高这项技能。”例子 我想学习/提高Flink。我是一个完全的初学者。 创建一个30天的学习计划&#xff0c;可以帮助像我这样的初…

Xilinx Artix-7【XC7A35T-2CSG324I】【XC7A35T-1CSG324I】成本与收发器优化的FPGA器件

产品介绍&#xff1a; Xilinx Artix -7系列 FPGA 重新定义了成本敏感型解决方案&#xff0c;功耗比上一代产品降低了一半&#xff0c;同时为高带宽应用提供一流的收发器和信号处理能力。这些设备基于 28 纳米 HPL 工艺构建&#xff0c;提供一流的性能功耗比。与 MicroBlaze™ 软…

boot-admin整合Liquibase实现数据库版本管理

Liquibase 和 Flyway 是两款成熟的、优秀的、开源/商业版的数据库版本管理工具&#xff0c;鉴于 Flyway 的社区版本对 Oracle 数据库支持存在限制&#xff0c;所以 boot-admin 选择整合 Liquibase 提供数据库版本管理能力支持。 Liquibase 开源版使用 Apache 2.0 协议。 Liqui…

实现服务器版本---表白墙(Servlet)

目录 一、创建Servlet项目 二、约定前后端交互接口 三、前端代码 四、后端代码 五、效果演示 结合Servlet API &#xff0c;实现一个服务器版本表白墙。实现的这个表白墙&#xff0c;就通过服务器来保存这里的消息数据&#xff0c;进而做到 “持久化” 存储。 一、创建Se…

浮点数中的阶码和尾数

阶码和尾数 阶码尾数浮点数浮点数表示示例 例题分析总结 阶码 在机器中表示一个浮点数时需要给出指数&#xff0c;这个指数用整数形式表示&#xff0c;这个整数叫做阶码。 尾数 常用对数的小数部分&#xff0c;用于科学计数法&#xff0c;其表示方法为:Mantissa x Base^Expo…

k210单片机的串口交互实验

先来看看实验的结果吧&#xff0c;k210的9口为RX&#xff0c;10口为TX。接线&#xff1a; 9口接usb转ttl的TX 10口接usb转ttl的RX 下面介绍一下k210需要使用的模块&#xff1a; K210 一共有 3 个串口&#xff0c;每个串口可以自由映射引脚。 例&#xff1a; # IO10→RX1&#…

JuiceFS__持久化缓存源码走读

JuiceFS__持久化缓存源码走读 JuiceFS 是一款高性能 POSIX 文件系统&#xff0c;针对云原生环境特别优化设计&#xff0c;在 Apache 2.0 开源协议下发布。使用 JuiceFS 存储数据&#xff0c;数据本身会被持久化在对象存储&#xff08;例如 Amazon S3&#xff09;&#xff0c;而…

java小记 2023-05-05

public class Test {/*** 谓类的方法就是指类中用static 修饰的方法&#xff08;非static 为实例方法&#xff09;&#xff0c;比如main 方法&#xff0c;那么可以以main* 方法为例&#xff0c;可直接调用其他类方法&#xff0c;必须通过实例调用实例方法&#xff0c;this 关键…

7.3 有源滤波电路(2)

四、开关电容滤波器 开关电容电路由受时钟脉冲信号控制的模拟开关、电容器和运算放大电路三部分组成。这种电路的特性与电容器的精度无关&#xff0c;而仅与各电容器电容量之比的准确性有关。在集成电路中&#xff0c;可以通过均匀地控制硅片上氧化层的介电常数及其厚度&#…

国产版ChatGPT大盘点

我们看到,最近,国内大厂开始密集发布类ChatGPT产品。 一方面,是因为这是最近10年最大的趋势和机会。 另一方面,国内的AI,不能别国外卡了脖子。 那在类ChatGPT赛道上,哪些中国版的ChatGPT能快速顶上?都各有哪些困境需要突破呢?本文给诸位带来各个玩家的最新进展。 *…

大数据Doris(十二):Unique数据模型

文章目录 Unique数据模型 一、读时合并 二、写时合并 Unique数据模型 在某些多维分析场景下,用户更关注的是如何保证 Key 的唯一性,即如何获得 Primary Key 唯一性约束。因此,我们引入了 Unique 数据模型,该模型可以根据相同的Primary Key 来保留后插入的数据,确保数据…

Day962.如何更好地重构和组织后端代码 -遗留系统现代化实战

如何更好地重构和组织后端代码 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录是关于如何更好地重构和组织后端代码的内容。 如果说在气泡上下文中开发新的需求&#xff0c;类似于老城区旁边建设一个新城区&#xff0c;那么在遗留系统中开发新的需求&#xff0c;就类似于在…

c++的构造函数与析构函数

构造函数是一种特殊的成员函数&#xff0c;用于在对象创建时初始化对象的成员变量。它的名称与类名相同&#xff0c;没有返回类型&#xff0c;可以有参数。当创建对象时&#xff0c;构造函数会自动调用&#xff0c;以初始化对象的成员变量。如果没有定义构造函数&#xff0c;编…

华为OD机试真题-24点运算【2023】【JAVA】

一、题目描述 计算24点是一种扑克牌益智游戏&#xff0c;随机抽出4张扑克牌&#xff0c;通过加()&#xff0c;减(-)&#xff0c;乘(*), 除(/)四种运算法则计算得到整数24&#xff0c;本问题中&#xff0c;扑克牌通过如下字符或者字符串表示&#xff0c;其中&#xff0c;小写jo…

PCL1.12.0+Vtk7.1.1安装

1. qt4&#xff1a;Ubuntu 20.04 LTS 安装qt4 library_ubuntu20.04安装qt4 2.本文下载过程可参考1&#xff1a;ubuntu20.04下安装pcl_ubuntu安装pcl_Yuannau_jk的博客-CSDN博客 参考2&#xff1a;Ubuntu 20.04.05安装PCL-1.12.0_no package metslib found_zhiTjun的博客-CSDN…