【JVM系列】- 穿插·对象的实例化与直接内存

对象的实例化与直接内存

😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🌝分享学习心得,欢迎指正,大家一起学习成长!

在这里插入图片描述

文章目录

  • 对象的实例化与直接内存
    • 创建对象的方式
      • ① 使用new关键字
      • ② 通过反射机制
      • ③ 使用克隆的方式
      • ④ 反序列化
    • 创建对象的步骤
      • ① 判断对象对应的类是否类加载
      • ② 为对象分配内存
      • ③ 处理并发安全问题
      • ④ 初始化
      • ⑤ 设置对象的对象头
      • ⑥ 执行init方法进行初始化
    • *对象的布局
      • 1). 对象头(Object Header)
      • 2). 实例数据(Instance Data)
      • 3). 对齐填充(Padding)
      • 案例理解
    • 对象的访问定位
      • 对象访问的两种方式
        • 1). 句柄访问
        • 2). 指针访问(Hotspot采用此方法)
    • 直接内存(Direct Memory)
      • 直接内存概述
      • 直接内存的性能
      • 直接内存的OOM
    • 总结

在Java中,对象实例化是创建一个类的实例的过程。

实例化(instantiate)是指在面向对象的编程中,把用类创建对象的过程称为实例化。是将一个抽象的概念类,具体到该类实物的过程。实例化过程中一般由**类名 对象名 = new 类名(参数1,参数2…参数n)**构成。

创建对象的方式

在Java中,创建对象的方式有许多种,简单的概括都有哪些方法。

① 使用new关键字

使用new 来创建方法是最常见的一种创建方式,这种方法会调用构造方法(默认是无参构造方法),如果有自定义构造方法,就会使用自定义构造方法来创建对象。

MyClass myObject = new MyClass();

还有一种常见的方式,就是使用类的静态方法,这种方式,实际上在静态方法内部也是使用了new的方式来创建,这里可以是无参构造也可以是自定义的含参构造器。
还有一种也是new方式的变形,就是通过一个专门的工厂类或者静态方法来创建对象。

MyClass myObject = MyClassFactory.createMyClass();

② 通过反射机制

使用 Java 的反射机制,可以在运行时获取类的信息并创建对象。但是这种方式只能是使用空参的构造器,权限还必须是public。

Class<?> myClass = Class.forName("com.example.MyClass");
MyClass myObject = (MyClass) myClass.newInstance();

在Java9之后就不推荐使用了,可以选择使用Constructor的newInstance(xx)。clazz.getDeclaredConstructor().newInstance(),这个也是反射的方式,可以调用空参、带参的构造器,对于权限没有要求。

③ 使用克隆的方式

通过实现 Cloneable 接口并覆盖 clone 方法,可以创建对象的副本。这种方式不调用任何构造器。

MyClass originalObject = new MyClass();
MyClass clonedObject = (MyClass) originalObject.clone();

④ 反序列化

反序列化是将对象从其序列化形式转换回原始对象的过程。在Java中,对象可以通过序列化将其状态保存到文件或通过网络传输。反序列化则是从这些序列化的数据中重新构建对象。

除了以上方法,还有其他的创建方式,比如三方库等。

创建对象的步骤

我们通过字节码来看一下实例化对象的过程。
在这里插入图片描述

对于上图中的Object s = new Object();,首先我们可以将其分成三个部分来看,第一个部分Object,这个是存放在方法区中的,也就是存储着类的信息、常量池、静态变量等等;第二部分就是s,这个是存在Java栈中,这是个引用对象,在栈中的本地变量表中存放对象的引用地址,指向Java的对象实例数据;而第三部分new Object(),这是对象的实例数据,存在Java堆中。
接下来分析一下字节码

// new创建一个对象,#2是常量池中对应 java/lang/Object 类的索引
0 new #2 <java/lang/Object>
// dup是复制栈顶元素。在这里,复制了刚刚创建的新对象的引用。
3 dup
// 调用对象的构造方法。#1 是常量池中对应 <init> 构造方法的索引,V 表示无返回值。这里实际上调用了 java/lang/Object 类的构造方法,对新创建的对象进行初始化。
4 invokespecial #1 <java/lang/Object.<init> : ()V>
// 将栈顶元素(即新创建的对象的引用)存储到本地变量1中。
7 astore_1
// 返回。这里返回的是 void 类型,因为在 Java 中构造方法没有显式的返回值。
8 return

以上是从字节码的过程,接下来用执行过程来介绍,以下分为6个步骤来。

① 判断对象对应的类是否类加载

首先,要实例化一个对象,必须加载其对应的类。类加载是Java程序启动的一部分。当程序首次引用一个类时,类加载器负责加载这个类的字节码到内存中。这就是加载类。类加载的下一个阶段是链接。在链接过程中,将为类的静态变量分配存储空间,并且如果存在父类,还会链接到父类。链接阶段将符号引用转化为直接引用。在链接中还有验证、准备、解析。 在类的初始化阶段,执行类构造器 方法。这是一个特殊的静态方法,由编译器生成,包含类的静态字段的初始化和静态代码块的执行。初始化是按需进行的,即只有在首次实例化类的对象或者首次访问类的静态成员时才会触发。如果一个类有父类,那么会首先初始化父类。

这个部分在之前的文章《类加载子系统与加载过程》就有描述过,这里就是简单复述一下。回归正传,当虚拟机遇到一条new指令的时候,首先就会先去检查这条指令的参数能否在Metaspace的常量池中,定位到一个类的符号引用(就如以上字节码中的0 new #2 <java/lang/Object>这行字节码,#2就是这个Object对象的符号引用),并且会检查符号引用对应的类是否已经被加载、解析和初始化。如果没有,将会重新类加载,会在双亲委派模式下,使用类加载器去查找对应的.class文件。如果没有找到就会抛出ClassNotFoundException异常。

② 为对象分配内存

一旦类初始化完成,JVM 就会为对象分配内存。内存分配的方式可以是在堆上分配,也可能是栈上分配,具体取决于对象的生命周期和大小。
再分配内存,首先需要计算对象占用的空间大小,接着就是在堆中划分一块内存给新对象,如果实例化成员变量是引用变量,仅分配引用变量空间即可,即4/8个字节大小。

关于字节大小,如果操作系统是64位
boolean:占1个字节、byte:占1个字节
char:占2个字节、short:占2个字节
int:占4个字节、float:占4个字节
long:占8个字节、double:占8个字节
引用变量:占8个字节

对于对象内存的分配,还需要看内存是否规整。
如果内存是规整的,那么JVM将采用的是指针碰撞(Pointer Bumping)[1]来为对象分配内存。

什么是指针碰撞?
指针碰撞(Pointer Bumping)是一种内存分配和垃圾回收的实现方式,通常用于实现可移植的、线程安全的垃圾回收器。它的工作原理是通过移动指针来分配和回收内存。在使用指针碰撞时,整个可用的内存被看作一个大的连续块。实际上意思就是将所有使用过的内存放在一边,空闲的内存存在另一边,中间放着一个指针作为分界点的指示器,分配内存的话,就是把指针向空闲内存挪动一段与对象大小相同的距离。如果垃圾收集器是选择Serial、ParNew这种基于压缩算法,虚拟机采用这种分配方式,一般使用带有compact(整理)过程的收集器是,使用指针碰撞。

然而,如果内存是不规整的,也就是说用过的和没用过的内存相互交错,虚拟机就会采用空闲列表法来为对象分配内存,也就是虚拟机就需要维护一个列表,记录了哪些内存块是可用的,再分配的时候会从列表中去找到一块足够大的空间去划分对象实例,并更新列表上的内容,这种的分配方式就是空闲列表(Free List)。

选择哪种分配方式是由Java堆是否规整决定的,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能来决定。

③ 处理并发安全问题

在JVM创建对象的过程中,是会涉及一些并发安全问题,并且也有一些解决策略。

  • 采用CAS失败重试、区域加锁保证原子性
    • 类加载是多线程环境下的一个关键问题。JVM使用类加载锁(Class Loading Lock)来保证在同一时刻只有一个线程能够加载一个类。这个锁是全局锁,确保每个类在同一时刻只能被一个线程加载。
  • 为每个线程都预分配一块TLAB
    • 通过-XX:+/-UserTLAB参数设定

④ 初始化

初始化分配到的空间就是最开始的默认初始化进行默认值设置,这能保证对象实例字段在不赋值时就可以直接使用。

属性赋值的操作有几部分,首先是属性的默认初始化,接着就是显示初始化、代码块中的初始化,然后是由构造器进行初始化。

⑤ 设置对象的对象头

在这一步就会将对象的所属类(元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储到对象的对象头中,这个过程的具体设置方式取决于JVM实现。

⑥ 执行init方法进行初始化

在Java程序的视角来看,初始化才是正式开始,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。这一部分就是将对象属性进行显示初始化、代码块中的初始化、构造器的初始化。

那么怎样才算对象创建完成呢?实际上,对象的创建从经历了加载类元信息,为对象分配内存,处理并发问题,属性的默认初始化,设置对象头的信息,数据的显示初始化、代码中初始化以及构造器初始化的几个过程之后才算对象已经创建完成,当然,如果说对象在默认初始化完毕就算创建完成也是可以的。

*对象的布局

在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)。在对象头中包含了标记字(mark word)、类指针(klass word)和 数组长度(array length)。synchronized主要是跟对象头有关系,也就是通过mark word的字节位数来表示各种锁状态。
未命名文件 (17).png

关于锁的知识点之前在《【多线程与高并发】- synchronized锁的认知》就已经介绍过了。

1). 对象头(Object Header)

在HotSpot虚拟机中,Java对象的头部包含了一些用于管理对象的元数据信息。其中主要是包含以下两个部分。

  • Mark Word(标记字): 占用 8 字节,包含了对象的一些状态信息,比如哈希值、锁状态标志、线程持有的锁、线程ID、偏向锁时间戳、GC 分代年龄等。Mark Word 的内容在对象的生命周期中可能会发生变化。
  • klass word(类型指针):Class对象的类型指针, 占用 4 字节或 8 字节,Jdk1.8默认开启指针压缩后为4字节,关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节1。其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址1。

如果是数组的话,还需要记录数组的长度。

2). 实例数据(Instance Data)

这是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(也包括了继承父类的和自身的字段)。如果存储的数组对象,会包含一个用于记录数组长度的字段以及存储了实际数组元素的部分。JVM会根据对象的布局灵活地调整内存布局,减小对象头和实例数据的总体大小,从而降低内存占用。对于相同宽度的字段会被分配到一起,父类定义的变量会出现在子类之前,如果CompactFields参数为true(也是默认值),子类的窄变量可能插入父类的变量空隙。

实例数据主要包括对象的各种成员变量,包括基本类型和引用类型。基本类型直接存储内容,引用类型则是存储的指针,static类型的变量会放到类中,而不是放到实例数据里。

3). 对齐填充(Padding)

对齐填充并不是必须的,这是一种优化的概念,是作为一种占位符的作用。主要是为了满足硬件对齐的要求,提高访问效率和性能。通过插入一些额外的字节,使得对象的起始地址符合特定的对齐要求。这样可以确保对象的实例数据按照硬件对齐规则排列,从而提高内存访问的效率。

案例理解

这里我准备一个案例,在Customer类中设置了几个变量,并继承父类User,其中有一个引用变量Account类。通过Start#main()来实现本次的案例。

// 父类
public class User {
    String username;
}
// 子类
public class Customer extends User {
    int id = 1;
    String name;
    Account account;
    {
        name = "默认客户";
    }

    public Customer() {
        account = new Account();
    }
}
// 引用的类
public class Account {
    int ids;
    double money;
    String tab;
}
public class Start {
    public static void main(String[] args) {
        Customer customer = new Customer();
        /*java对象的内存布局以及使用ClassLayout查看布局*/
        System.out.println("Customer:" + ClassLayout.parseInstance(customer).toPrintable());    
    }
}

这里我引入了ClassLayout来查看Java对象内存布局的,需要引入以下坐标。

<!--  java对象的内存布局以及使用ClassLayout查看布局  -->
<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.17</version>
</dependency>

通过运行Start#main()可以清晰看到所占用的字节信息,对于Customer对象,继承了User类,在创建实例的时候会把父类的属性也一并加载过来。首先,对象头是固定的8(标记字)+4(类型指针)字节,实例数据由于会加载父类属性,一共是16字节(对于引用类型,JVM是64位应该是占8字节,但是采用了指针压缩,所以只占了4字节),8+4+16=28字节,不满足对齐要求,因此还需要引入对齐填充占4个字节,一共就是32字节。

Customercom.lyd.testboot.jvm.objectdemo.Customer object internals:
OFF  SZ                                      TYPE DESCRIPTION               VALUE
  0   8                                           (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4                                           (object header: class)    0xf800c182
 12   4                          java.lang.String User.username             null
 16   4                                       int Customer.id               1
 20   4                          java.lang.String Customer.name             (object)
 24   4   com.lyd.testboot.jvm.objectdemo.Account Customer.account          (object)
 28   4                                           (object alignment gap)    
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从以上打印出来的日志能够看到实例中包含了父类与本类的属性以及所占字节数。下图就来介绍整个对象的实例过程。
在这里插入图片描述

每个方法就是一个栈帧,当我们运行Start#main(),因为是main方法,在栈帧中的局部变量表中会先加载args,里面Customer customer = new Customer();会将customer记录到栈帧的局部变量表中,通过地址指针指向了堆中的整个实例对象。new Customer()这个实例的内部结构在堆空间记录了对象头、实例数据、填充字节。在该实例的对象头部中,记录了标记字(主要用与GC、线程安全等),通过类型指针指向方法区中对应Customer的klass类元信息。在实例数据中,会加载父类的属性以及本身属性,Customer类中引用了String对象,这是个引用类型,通过静态代码块赋值,这个字符串存在自负床变量池中。其中也引用了Account对象,这也是个引用类型,并不会将此对象属性都加载进来,只是记录了这个对象的引用地址,指向堆空间中的new Account()实例,在Account实例中,也是与Customer实例一样,包含了对象头等信息,也是通过类型指针指向方法区Account的klass类元信息。

对象的访问定位

从上文介绍对象的布局就已经能够知道对象是如何获取对象实例的。对于一个Customer customer = new Customer();我们知道其存储为三个部分,在栈帧中存储了堆区中的引用地址(reference),在堆区有个元数据指针(InstanceOopDesc)指向方法区中的InstanceKlass。总的来说,就是栈帧中记录着堆区实例化的对象地址,通过这个地址来方法对象实例。

对象访问的两种方式

对象的访问方式主要有两种方式,句柄访问和指针访问,在hotspot虚拟机中采用的是指针访问的方式。

1). 句柄访问

有些 JVM 实现可能使用了句柄访问的概念,其中对象的引用由一个句柄对象来管理,而句柄包含了对象的地址以及其他元信息。在Java堆中会开辟一个句柄池与实例池,句柄池中会通过指针指向实例对象和对象类型。
对象访问-句柄访问.png

因为采用的是句柄池指向对象实例数据,在reference中存储的是稳定的句柄地址,对象如果被移动(垃圾收集时候会移动对象)只会改变句柄的实例数据指针就行,reference本身是不用做修改。但是需要开辟一个空间来充当句柄池,这就会增加堆内存的空间占用。

2). 指针访问(Hotspot采用此方法)

在栈帧中的本地变量表中记录了堆中对象实例数据的地址,通过地址引用到堆中的对象实例数据,实例对象在通过类型指针指向方法区中的类元信息。
对象访问-指针引用.png

指针方式不用额外占用堆的空间,但是如果遇到对象移动,就需要去修改reference存储的地址。

直接内存(Direct Memory)

直接内存不是JVM运行时数据区的一个部分,也不是《Java虚拟机规范》中定义的内存区域,它是Java堆外的、直接向系统获取的内存空间。

直接内存概述

在Java虚拟机(JVM)中,“直接内存” 通常指的是使用 NIO(New I/O | Non-Blocking I/O)包中的 ByteBuffer 类,以及其中的 allocateDirect 方法所分配的直接字节缓冲区(Direct ByteBuffer)。通过DirectByteBuffer操作Native内存。

public class BufferTest {
    private static final int BUFFER_SIZE = 1024 * 1024 * 1024;

    public static void main(String[] args) {
        // byteBuffer 将持有一个大小为 1 GB 的直接内存缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
        System.out.println("内存分配:" + BUFFER_SIZE + "byte");
        Scanner sc = new Scanner(System.in);
        sc.next();
        System.out.println("释放内存");
        byteBuffer = null;
        System.gc();
    }
}

以上代码,我们可以创建出一个内存为1G的直接内存,这里通过Scanner输入进行阻塞,我们可以通过进程查看所占用的内存大小。可见使用ByteBuffer#allocateDirect()会直接分配本地内存。
在这里插入图片描述

直接内存的性能

通常,直接内存的速度会比Java堆更快,读写性能高。在一些频繁使用IO的场景,可能会考虑使用直接内存。Java的NIO包是允许程序使用直接内存,用来作为数据缓冲区。
对于非直接缓冲区,读写文件需要与磁盘交互,这时就需要由用户态切换到内核态,在由内核态去对物理磁盘进行交互。这样就造成了需要进行两份内存的存储重复数据,效率低。
在这里插入图片描述

对于直接缓冲区,使用NIO就能够直接的使用操作系统给出的缓存区,只会存储一份。
在这里插入图片描述

直接内存的OOM

直接内存也有可能导致OutOfMemoryError异常。因为直接内存是存在Java堆外的,他的大小不会受限于-Xmx设置的最大堆空间,系统内存是有限的,Java堆和直接内存加起来不能超过操作系统给的最大内存。它是受限于操作系统对进程的可用虚拟内存空间。
如果是超过内存的限制,就会抛出java.lang.OutOfMemoryError: Direct buffer memory
缺点:

  • 分配回收成本比较高
  • 不受JVM的内存回收管理

直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认是与堆的最大值-Xmx参数值一致。

总结

本此学习穿插了Java对象的内存布局,更加清楚了解到对象的创建方式以及过程,最为重要的是了解对象的布局结构,包括实例对象数据存放在堆中,类元信息在方法区,栈帧通过引用去指向对应的数据信息。对比了句柄方式和指针方式。最后学习了直接内存的内容,了解了直接内存也是会出现OOM异常。

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

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

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

相关文章

Java多线程二-线程安全

1、线程安全问题 多个线程&#xff0c;同时操作同一个共享资源的时候&#xff0c;可能会出现业务安全问题。 2、实例&#xff1a;取钱的线程安全问题 2.1、场景 小明和小红是夫妻&#xff0c;他们有个共同账户&#xff0c;余额是十万元&#xff0c;如果两人同时取钱并且各自取…

Nodejs 第二十章(fs 上)

概述 在 Node.js 中&#xff0c;fs 模块是文件系统模块&#xff08;File System module&#xff09;的缩写&#xff0c;它提供了与文件系统进行交互的各种功能。通过 fs 模块&#xff0c;你可以执行诸如读取文件、写入文件、更改文件权限、创建目录等操作&#xff0c;Node.js …

Mindomo Desktop for Mac免费思维导图软件,助您高效整理思维

思维导图是一种强大的工具&#xff0c;可以帮助我们整理思维、提高记忆力、激发创造力。而Mindomo Desktop for Mac作为一款免费的思维导图软件&#xff0c;能够帮助我们更高效地进行思维整理和项目管理。在本文中&#xff0c;我们将介绍Mindomo Desktop for Mac的功能和优势&a…

汽车租聘管理与推荐系统Python+Django网页界面+协同过滤推荐算法

一、介绍 汽车租聘管理与推荐系统。本系统使用Python作为主要编程语言&#xff0c;前端采用HTML、CSS、BootStrap等技术搭建前端界面&#xff0c;后端采用Django框架处理用户的请求。创新点&#xff1a;使用协同过滤推荐算法实现对当前用户个性化推荐。 其主要功能如下&#x…

频剪辑软件Corel VideoStudio 会声会影2024最新7大新全新功能解析

我很喜欢视频剪辑软件Corel VideoStudio 会声会影2024&#xff0c;因为它使用起来很有趣。它很容易使用&#xff0c;但仍然给你很多功能和力量。视频剪辑软件Corel VideoStudio 会声会影2023让我与世界分享我的想法&#xff01;“这个产品的功能非常多&#xff0c;我几乎没有触…

docker介绍、部署与常用命令

一、docker 介绍 1、容器&#xff08;Container&#xff09;&#xff1a; (1) 概念&#xff1a; 容器是一种用于运行和部署应用程序的技术。它将应用程序及其所有依赖项&#xff08;例如代码、运行时、系统工具、系统库等&#xff09;打包在一个独立的、可移植的运行环境中&…

[C++]指针与结构体

标题 一.指针1.指针的定义和使用2.指针所占的内存空间3.空指针与野指针4.const修饰指针5.指针和数组6.指针和函数 二.结构体1.结构体的定义与使用2.结构体数组3.结构体指针4.结构体的嵌套使用5.结构体做函数参数6.结构体中const使用场景7.案例练习 一.指针 作用: 可以通过指针…

SpringBoot 拦截器高级篇

Springboot 拦截器 定义使用场景拦截器与过滤器的区别实现步骤全局拦截器的局限性全局拦截器VS局部拦截器局部拦截器自定义局部拦截器使用多个局部拦截器 定义 拦截器是Spring MVC框架中的一个重要组件&#xff0c;它是一种AOP&#xff08;面向切面编程&#xff09;的实现方式&…

文本编辑 UTF-8 BOM 中的BOM释义

参考资料 UTF8のBOM無しとBOM付きの違いBOMなしUTF-8によってWindowsでもたらされる困惑文字コードをUTF-8 BOMなし(UTF-8N)でファイル保存をする方法 目录 一. 前提二. BOM三. CSV文件中的表现 一. 前提 在使用Windows自带的记事本编辑.csv文件的时候&#xff0c;准备保存为…

Linux安装jdk8【十分丝滑】

1.上传安装包到Linux&#x1f495;&#x1f495;&#x1f495; 2.使用命令解压缩&#x1f495;&#x1f495;&#x1f495; tar -zxvf 压缩文件名 3.重命名&#x1f495;&#x1f495;&#x1f495; mv 原文件名 新的文件名 4.配置环境变量&#x1f929;&#x1f929;&…

Labelme加载AI(Segment-Anything)模型进行图像标注

labelme是使用python写的基于QT的跨平台图像标注工具&#xff0c;可用来标注分类、检测、分割、关键点等常见的视觉任务&#xff0c;支持VOC格式和COCO等的导出&#xff0c;代码简单易读&#xff0c;是非常利用上手的良心工具。 第一步&#xff1a;   下载源码进行安装。 g…

PaddleOCR学习笔记

Paddle 功能特性 PP-OCR系列模型列表 https://github.com/PaddlePaddle/PaddleOCR#%EF%B8%8F-pp-ocr%E7%B3%BB%E5%88%97%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8%E6%9B%B4%E6%96%B0%E4%B8%AD PP-OCR系列模型列表&#xff08;V4&#xff0c;2023年8月1日更新&#xff09; 配置文…

Spring如何在多线程下保持事务的一致性

Spring如何在多线程下保持事务的一致性 方法&#xff1a;每个线程都开启各自的事务去执行相关业务&#xff0c;等待所有线程的业务执行完成&#xff0c;统一提交或回滚。 下面我们通过具体的案例来演示Spring如何在多线程下保持事务的一致性。 1、项目结构 2、数据库SQL CR…

Unity 场景切换

Unity场景切换可使用以下方法&#xff1a; 1、SceneManager.LoadScene()方法&#xff1a; using UnityEngine.SceneManagement;// 切换到Scene2场景 SceneManager.LoadScene("Scene2"); 2、使用SceneManager.LoadSceneAsync()方法异步加载场景&#xff0c;异步加载…

Go 语言 Printf 函数和格式化动词详解

Printf() 函数可以使用多种格式化动词对输出进行格式化。下面是可以与所有数据类型一起使用的一些通用格式化动词&#xff1a; 通用格式化动词&#xff1a; 以下动词适用于所有数据类型&#xff1a; 动词描述%v以默认格式打印值%#v以 Go 语法格式打印值%T打印值的类型%%打印百…

灯塔的安装

Docker 安装 docker 安装参考&#xff1a;https://docs.docker.com/engine/install/ shell脚本: curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh灯塔安装 mkdir docker-ARL;cd docker-ARL curl https://bootstrap.pypa.io/get-pip.py -o get-pip…

服务器下db(数据库)的执行

1、查看 select * from xxxx&#xff08;表名&#xff09; where xxx&#xff08;列表&#xff09;1 and.......正常写就行 2、插入 如果你想要在 SELECT INSERT INTO … SELECT 语句中将部分列保持不变,只改变一两列的值,可以在 语句中直接设置目标列的值,而其他列从源表中…

thinkphp最新开发的物业管理系统 缴费管理、停车管理、收费管理、值班管理

物业费&#xff0c;水电燃气费&#xff0c;电梯费&#xff0c;租金&#xff0c;临时收费等多种收费规则完全自定义&#xff0c;账单自动生成&#xff0c;无需人工计算 实时数据互通&#xff1a;一键报事报修&#xff0c;购买车辆月卡&#xff0c;管理家人信息&#xff0c;参加物…

知识的分层:know-what/how/why

知识&#xff08;knowledge&#xff09;表示知道某些信息。通常而言&#xff0c;知识是正确的&#xff0c;但不一定是完备的。知识本身有自己的适用范围&#xff0c;特别是工程技术类问题。 根据知识的类型&#xff0c;可分为三类&#xff1a; know-whatknow-howknow-why kno…

个人成长|信奉长期主义,就要多做可积累有复利的事。

哈喽啊&#xff0c;大家好&#xff0c;我是雷工&#xff01; 最近有个哥们儿吐槽&#xff0c;说他们公司人事找他谈话&#xff0c;要给降工资&#xff0c;他不同意。 过了没几天又说&#xff1a; “定了&#xff0c;全员降薪”。 “你同意了&#xff1f;” “不同意&#xff0…
最新文章