当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!

一、写在开头

依稀记得多年以前的一场面试中,面试官从Java并发编程问到了锁,从锁问到了原子性,从原子性问到了Atomic类库(对着JUC包进行了刨根问底),从Atomic问到了CAS算法,紧接着又有追问到了底层的Unsafe类,当问到Unsafe类时,我就知道这场面试废了,这似乎把祖坟都能给问冒烟啊。

但时过境迁,现在再回想其那场面试,不再觉得面试官的追毛求疵,反而为那时候青涩菜鸡的自己感到羞愧,为什么这样说呢,实事求是的说Unsafe类虽然是比较底层,并且我们日常开发不可能用到的类,但是!翻开JUC包中的很多工具类,只要底层用到了CAS思想来提升并发性能的,几乎都脱离不了Unsafe类的运用,可惜那时候光知道被八股文了,没有做到细心总结与发现。

二、Unsafe的基本介绍

我们知道C语言可以通过指针去操作内存空间,Java不存在指针,为了提升Java运行效率、增强Java语言底层资源操作能力,便诞生了Unsafe类,Unsafe是位于sun.misc包下。正如它的名字一样,这种操作底层的方式是不安全的,在程序中过度和不合理的使用,会带来未知的风险,因此,Unsafe虽然,但要慎用哦!

2.1 如何创建一个unsafe实例

我们无法直接通过new的方式创建一个unsafe的实例,为什么呢?我们看它的这段源码便知:

public final class Unsafe {
  // 单例对象
  private static final Unsafe theUnsafe;

  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 仅在启动类加载器`BootstrapClassLoader`加载时才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

从源码中我们发现Unsafe类被final修饰,所以无法被继承,同时它的无参构造方法被private修饰,也无法通过new去直接实例化,不过在Unsafe 类提供了一个静态方法getUnsafe,看上去貌似可以用它来获取 Unsafe 实例。但是!当我们直接去调用这个方法的时候,会报如下错误:

Exception in thread "main" java.lang.SecurityException: Unsafe
  at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
  at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)

这是因为在getUnsafe方法中,会对调用者的classLoader进行检查,判断当前类是否由Bootstrap classLoader加载,如果不是的话就会抛出一个SecurityException异常。

那我们如果想使用Unsafe类,到底怎样才能获取它的实例呢?

在这里提供给大家两种方式:

方式一

假若在A类中调用Unsafe实例,则可通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被启动类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相关方法的类所在jar包路径 

方式二

利用反射获得 Unsafe 类中已经实例化完成的单例对象:

public static Unsafe getUnsafe() throws IllegalAccessException {
     Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
     //Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同
     unsafeField.setAccessible(true);
     Unsafe unsafe =(Unsafe) unsafeField.get(null);
     return unsafe;
 }

2.2 Unsafe的使用

上面我们已经知道了如何获取一个unsafe实例了,那现在就开始写一个小demo来感受一下它的使用吧。

public class TestService {
    //通过单例获取实例
    public static Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
        Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        //Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以这样,作用相同
        unsafeField.setAccessible(true);
        Unsafe unsafe =(Unsafe) unsafeField.get(null);
        return unsafe;
    }
    //调用实例方法去赋值
    public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
        Persion persion = new Persion();
        persion.setAge(10);
        System.out.println("ofigin_age:" + persion.getAge());
        long fieldOffset = unsafe.objectFieldOffset(Persion.class.getDeclaredField("age"));
        System.out.println("offset:"+fieldOffset);
        unsafe.putInt(persion,fieldOffset,20);
        System.out.println("new_age:"+unsafe.getInt(persion,fieldOffset));
    }

    public static void main(String[] args) {
        TestService testService = new TestService();
        try {
            testService.fieldTest(getUnsafe());
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
class Persion{

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

输出:

ofigin_age:10
offset:12
new_age:20

通过 Unsafe 类的objectFieldOffset方法获取到了对象中字段的偏移地址,这个偏移地址不是内存中的绝对地址而是一个相对地址,之后再通过这个偏移地址对int类型字段的属性值进行读写操作,通过结果也可以看到 Unsafe 的方法和类中的get方法获取到的值是相同的。

三、Unsafe类的8种应用

基于Unsafe所提供的API,我们大致可以将Unsafe根据应用场景分为如下的八类,上一个脑图。
在这里插入图片描述

3.1 内存操作

学习过C或者C++的同学对于内存操作应该很熟悉了,在Java里我们是无法直接对内存进行操作的,我们创建的对象几乎都在堆内内存中存放,它的内存分配与管理都是JVM去实现,同时,在Java中还存在一个JVM管控之外的内存区域叫做“堆外内存”,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法啦。

内存操作的常用方法:

/*包含堆外内存的分配、拷贝、释放、给定地址值操作*/
//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

在这里我们不仅会想,为啥全是native方法呢?

  1. native方法通过JNI调用了其他语言,如果C++等提供的现车功能,可以让Java拿来即用;
  2. 需要用到 Java 中不具备的依赖于操作系统的特性,Java 在实现跨平台的同时要实现对底层的控制,需要借助其他语言发挥作用;
  3. 程序对时间敏感或对性能要求非常高时,有必要使用更加底层的语言,例如 C/C++甚至是汇编。

【经典应用】
在Netty、MINA等NIO框架中我们常常会应到缓冲池,而实现缓冲池的一个重要类就是DirectByteBuffer,它主要的作用对于堆外内存的创建、使用、销毁等工作。

通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存

在这里插入图片描述
在这里插入图片描述
从上图我们可以看到,在构建实例时,DirectByteBuffer内部通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。

3.2 内存屏障

为了充分利用缓存,提高程序的执行速度,编译器在底层执行的时候,会进行指令重排序的优化操作,但这种优化,在有些时候会带来 有序性 的问题。(在将volatile关键字的时候提到过了)

为了解决这一问题,Java中引入了内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令),通过组织屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。

在Unsafe类中提供了3个native方法来实现内存屏障:

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

【经典应用】
在之前的文章中,我们讲过Java8中引入的一个高性能的读写锁:StampedLock(锁王),在这个锁中同时支持悲观读与乐观读,悲观读就和ReentrantLock一致,乐观读中就使用到了unsafe的loadFence(),一起去看一下。

	/**
     * 使用乐观读锁访问共享资源
     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候					可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @return
     */
   double distanceFromOrigin() {
     long stamp = sl.tryOptimisticRead(); // 获取乐观读锁
     double currentX = x, currentY = y;	// 拷贝共享资源到本地方法栈中
     if (!sl.validate(stamp)) { // //检查乐观读锁后是否有其他写锁发生,有则返回false
        stamp = sl.readLock(); // 获取一个悲观读锁
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp); // 释放悲观读锁
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

在官网给出的乐观读的使用案例中,我们看到if中做了一个根绝印章校验写锁发生的操作,我们跟入这个校验源码中:

public boolean validate(long stamp) {
        U.loadFence();//load内存屏障
        return (stamp & SBITS) == (state & SBITS);
    }

这一步的目的是防止锁状态校验运算发生重排序导致锁状态校验不准确的问题!

3.3 对象操作

其实在2.2 Unsafe的使用中,我们已经使用了Unsafe进行对象成员属性的内存偏移量获取,以及字段属性值的修改功能了,除了Int类型,Unsafe还支持对所有8种基本数据类型以及Object的内存数据修改,这里就不再赘述了。

需要额外强掉的一点,在Unsafe的源码中还提供了一种非常规的方式进行对象的实例化:

//绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

这种方法可以绕过构造方法和初始化代码块来创建对象,我们写一个小demo学习一下。

@Data
 public class A {
     private int b;
     public A(){
         this.b =1;
     }
 }

定义一个类A,我们分别采用无参构造器、newInstance()、Unsafe方法进行实例化。

public void objTest() throws Exception{
     A a1=new A();
     System.out.println(a1.getB());
     A a2 = A.class.newInstance();
     System.out.println(a2.getB());
     A a3= (A) unsafe.allocateInstance(A.class);
     System.out.println(a3.getB());
 }

输出结果为1,1,0。这说明调用unsafe的allocateInstance方法确实可以跳过构造器去实例化对象!

3.4 数组操作

在 Unsafe 中,可以使用arrayBaseOffset方法获取数组中第一个元素的偏移地址,使用arrayIndexScale方法可以获取数组中元素间的偏移地址增量,通过这两个方法可以定位数组中的每个元素在内存中的位置。

基于2.2 Unsafe使用的测试代码,我们增加如下的方法:

  //获取数组元素在内存中的偏移地址,以及偏移量
    private void arrayTest(Unsafe unsafe) {
        String[] array=new String[]{"aaa","bb","cc"};
        int baseOffset = unsafe.arrayBaseOffset(String[].class);
        System.out.println("数组第一个元素的偏移地址:" + baseOffset);
        int scale = unsafe.arrayIndexScale(String[].class);
        System.out.println("元素偏移量" + scale);

        for (int i = 0; i < array.length; i++) {
            int offset=baseOffset+scale*i;
            System.out.println(offset+" : "+unsafe.getObject(array,offset));
        }
    }

输出:

数组第一个元素的偏移地址:16
元素偏移量4
16 : aaa
20 : bb
24 : cc

3.5 CAS相关

终于,重点来了,我们写这篇文章的初衷是什么?是回想起曾经面时,面试官由原子类库(Atomic)问到了CAS算法,从而追问到了Unsafe类上,在JUC包中到处都可以看到CAS的身影,在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等等类中均有!

以AtomicInteger为例,在内部提供了一个方法为compareAndSet(int expect, int update) ,如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update),而它的底层调用则是unsafe的compareAndSwapInt()方法。

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

CAS思想的底层实现其实就是Unsafe类中的几个native本地方法:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

3.6 线程调度

Unsafe 类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度,在前面介绍 AQS 的文章中我们学过,在AQS中通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

LockSupport源码:

public static void park(Object blocker) {
     Thread t = Thread.currentThread();
     setBlocker(t, blocker);
     UNSAFE.park(false, 0L);
     setBlocker(t, null);
 }
 public static void unpark(Thread thread) {
     if (thread != null)
         UNSAFE.unpark(thread);
 }

3.7 Class操作

Unsafe 对Class的相关操作主要包括静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
//获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

【测试案例】

@Data
 public class User {
     public static String name="javabuild";
     int age;
 }
 private void staticTest() throws Exception {
     User user=new User();
     //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用
     System.out.println(unsafe.shouldBeInitialized(User.class));
     Field sexField = User.class.getDeclaredField("name");
     //获取给定静态字段的内存地址偏移量
     long fieldOffset = unsafe.staticFieldOffset(sexField);
     //获取一个静态类中给定字段的对象指针
     Object fieldBase = unsafe.staticFieldBase(sexField);
     //根据某个字段对象指针和偏移量可以唯一定位这个字段。
     Object object = unsafe.getObject(fieldBase, fieldOffset);
     System.out.println(object);
 }

此外,在Java8中引入的Lambda表达式的实现中也使用到了defineClass和defineAnonymousClass方法。

3.8 系统信息

Unsafe 中提供的addressSize和pageSize方法用于获取系统信息。

1) 调用addressSize方法会返回系统指针的大小,如果在 64 位系统下默认会返回 8,而 32 位系统则会返回 4。

2) 调用 pageSize 方法会返回内存页的大小,值为 2 的整数幂。

使用下面的代码可以直接进行打印:

private void systemTest() {
     System.out.println(unsafe.addressSize());
     System.out.println(unsafe.pageSize());
}

输出为:8,4096

四、总结

哎呀,妈呀,终于写完了,人要傻了,为了整理这篇文章看了大量的源码,人看的头大,跟俄罗斯套娃似的源码,严谨的串联在一起!Unsafe类在日常的面试中确实不经常被问到,大家稍微了解一下即可。

五、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
在这里插入图片描述
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
在这里插入图片描述

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

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

相关文章

用本地大模型(llama3)进行数据分类标记|LLM 用例

001&#xff5c;背景 这是我最新发布的导航站点 https://aiwith.me 这个站点由于第一版数据来源于第三方&#xff0c;只有站点基础数据&#xff0c;没有对数据进行分类&#xff0c;最近站点上线了&#xff0c;主体框架都搭建完成了&#xff0c;这才有空去对数据做分类. 目前的…

USART串口通信(stm32)

一、串口通信 通信的目的&#xff1a;将一个设备的数据传送到另一个设备&#xff0c;扩展硬件系统 通信协议&#xff1a;制定通信的规则&#xff0c;通信双方按照协议规则进行数据收发 STM32F103C8T6 USART资源&#xff1a; USART1、 USART2、 USART3 自带波特率发生器&…

数据结构:二叉树与树

一 树的基本概念&#xff1a; 1.树的形状&#xff1a; 2.树的定义&#xff1a; 树是一种非线性的数据结构&#xff0c;它是n(n > 0)个结点的有限集。当n 0时&#xff0c;称为空树。在任意一棵非空树中应满足&#xff1a; 2.1 有且仅有一个特定的称为根的结点。 2.2 当n …

【React】Ant Design 5.x版本table scroll滚动条样式自定义

新版里table的滚动条样式由:scrollbar-width、scrollbar-color接管&#xff0c;导致之前的伪类方式失效&#xff01; 环境 antd: ^5.14.1react: ^18 解决方案&#xff1a; 若想延续伪类方式修改滚动条样式&#xff0c;可以在定义伪类样式文件里加入以下片段&#xff1a; .a…

什么是JDK21虚拟线程

JDK21虚拟线程 1. 来一段小故事2. 什么是虚拟线程3. 虚拟线程的几个关键特点4.细说关键特点1.为什么轻量级的1.传统线程运行时间2.虚拟线程运行时间3.对垃圾回收的影响 2.非绑定OS线程的魅力所在3.和传统相比为何易于使用4.阻塞优化有什么好处1.什么是阻塞优化2.JDK 21虚拟线程…

配置docker阿里云镜像地址

一、安装docker的步骤&#xff1a; 1.yum install -y yum-utils 2.yum-config-manager --add-repo http://mirrors.aliyun.com/docker- ce/linux/centos/docker-ce.repo --配置阿里云仓库3.yum makecache fast4.yum install docker-ce -y5.docker version …

【LVGL_Linux安装NXP的Gui-Guider】

GUI Guider是恩智浦为LVGL开发了一个上位机GUI设计工具&#xff0c;可以通过拖放控件的方式设计LVGL GUI页面&#xff0c;加速GUI的设计。 虽然他只支持自家芯片&#xff0c;但是应用层我们可以直接拿来用作其他MCU上。 GUI-Guider 下载 NXP官网下载&#xff1a;链接&#xff1…

关于sklearn决策树手动指定节点进行剪枝调整的实现

一、决策树剪枝 决策树的剪枝方式有两种&#xff0c;预剪枝和后剪枝&#xff0c;后剪枝在python的sklearn方法中提供了CCP代价复杂度剪枝法&#xff08;Cost Complexity Pruning&#xff09;具体实现代码如下&#xff1a; # -*- coding: utf-8 -*- from sklearn.datasets imp…

AI模型抉择:开源VS闭源,谁主沉浮?

AI模型抉择&#xff1a;开源VS闭源&#xff0c;谁主沉浮&#xff1f; &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &am…

软件设计师备考 | 案例专题之数据库设计 概念与例题

相关概念 关注上图中的两个部分&#xff1a; 概念结构设计 设计E-R图&#xff0c;也即实体-联系图。 工作步骤&#xff1a;选择局部应用、逐一设计分E-R图、E-R图合并。进行合并时&#xff0c;它们之间存在的冲突主要有以下3类&#xff1a; 属性冲突。同一属性可能会存在于…

如何通过OpenHarmony的音频模块实现录音变速功能?

简介 OpenAtom OpenHarmony&#xff08;以下简称“OpenHarmony”&#xff09;是由开放原子开源基金会孵化及运营的开源项目&#xff0c;是面向全场景、全连接、全智能时代的智能物联网操作系统。 多媒体子系统是OpenHarmony系统中的核心子系统&#xff0c;为系统提供了相机、…

MySQL中视图是什么,有什么作用

目录 一、视图的简介 1.1 什么是视图&#xff1f; 1.2 为什么使用视图&#xff1f; 1.3 视图有哪些规则与限制&#xff1f; 1.4 视图能否更新&#xff1f; 二、视图的创建 三、视图的作用 3.1 用视图简化复杂的联结 3.2 用视图格式化检索出的数据 3.3 用视图过滤数据…

redisson 使用fastJson2序列化

前因&#xff1a;一个项目&#xff1a;有人用redisTemplete存数据&#xff08;使用了fastjson2&#xff09;&#xff0c;使用redisson取的时候就会报错。要让redisTemplete与redisson序列化一致 一、自定义序列化器 import com.alibaba.fastjson2.JSON; import com.alibaba.fa…

LeetCode //C - 119. Pascal‘s Triangle II

119. Pascal’s Triangle II Given an integer rowIndex, return the rowIndexth (0-indexed) row of the Pascal’s triangle. In Pascal’s triangle, each number is the sum of the two numbers directly above it as shown: Example 1: Input: rowIndex 3 Output: …

集中抄表电表是什么?

1.集中抄表电表&#xff1a;简述 集中抄表电表&#xff0c;又称为远程抄表系统&#xff0c;是一种现代化电力计量技术&#xff0c;为提升电力行业的经营效率和客户服务质量。它通过自动化的形式&#xff0c;取代了传统人工抄水表&#xff0c;完成了数据信息实时、精确、高效率…

C# 深拷贝和浅拷贝

文章目录 1.深拷贝2.浅拷贝3.拷贝类4.浅拷贝的实现5.深拷贝实现5.1 浅拷贝对象&#xff0c;对引用类型重新一个个赋值5.2 反射实现5.3 利用XML序列化和反序列化实现 1.深拷贝 拷贝一个对象时&#xff0c;不仅仅把对象的引用进行复制&#xff0c;还把该对象引用的值也一起拷贝。…

Python基于PyQt6制作GUI界面——按钮

示例对应的制作的 ui文件 界面如下所示。 <?xml version"1.0" encoding"UTF-8"?> <ui version"4.0"><class>Form</class><widget class"QWidget" name"Form"><property name"geom…

C#使用开源操作库MiniExcel操作Excel

简介 MiniExcel 简单、高效避免 OOM 的.NET 处理 Excel 查、写、填充数据工具。 目前主流框架大多需要将数据全载入到内存方便操作&#xff0c;但这会导致内存消耗问题&#xff0c;MiniExcel 尝试以 Stream 角度写底层算法逻辑&#xff0c;能让原本 1000 多 MB 占用降低到几 …

2024年【N1叉车司机】考试题及N1叉车司机找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 N1叉车司机考试题参考答案及N1叉车司机考试试题解析是安全生产模拟考试一点通题库老师及N1叉车司机操作证已考过的学员汇总&#xff0c;相对有效帮助N1叉车司机找解析学员顺利通过考试。 1、【多选题】《特种设备使用…

微服务-系统架构

微服务&#xff1a; 系统架构的演变 单一应用架构 早期的互联网应用架构&#xff0c;大量应用服务 功能 集中在一个包里&#xff0c;把大量的应用打包为一个jar包&#xff0c;部署在一台服务器&#xff0c;例如tomcat上部署Javaweb项目 缺点:耦合度高&#xff0c;一台服务器…