用友2024秋招后端BIP一面-2023.8.10

1 自我介绍

。。。

2 实习项目-实例反向同步到定义

2.1 你这个只涉及到数据同步是吧,表结构有差异吗,比如在实例表上A字段要对应的是A字段,但是在定义表上对应的是B字段的

答:有,比如说当涉及到第三方表的外键,而这个外键刚好也是同步表中的主键,那么我这个主键的时候不可能一摸一样的同步过去,还需要根据架构名以及其他的路径信息去定义表中查询最新的定义主键,所以一般这种存在依赖关系,定义更新的时候也要注意顺序,比如一个板卡是一个实体的卡槽,这个卡槽还需要插入相应的接线块,那么卡槽应该先同步好,然后接线块在查找其应用的板卡时才能在板卡定义表中查找到最新的同步值

2.2 导师安排的需求还是你觉得有必要做然后就交付了呢?

答:导师安排的

3 手撸spring项目

3.1 你觉得比较两眼的地方?

答:一级缓存如何解决循环依赖问题

3.2 讲一下原来的三级缓存的类加载机制,然后讲讲你是怎么发现一级缓存就可以避免这个问题的

答:一般的三级缓存解决循环依赖依赖过程:

推荐文章:

作者:变速风声

链接:https://juejin.cn/post/7099745254743474212

来源:稀土掘金

在上文章节铺垫的基础上,此处结合一个循环依赖的案例,分析下如何使用三级缓存解决单例 Bean 的循环依赖。

  1. 创建对象 A,完成生命周期的第一步,即实例化(Instantiation),在调用

createBeanInstance 方法后,会调用 addSingletonFactory 方法,将已实例化但未属性赋值未初始化的对象 A 放入三级缓存 singletonFactories 中。即将对象 A 提早曝光给 IoC 容器。

  1. 继续,执行对象 A 生命周期的第二步,即属性赋值(Populate)。此时,发现对象 A 依赖对象,所以就会尝试去获取对象 B。
  2. 继续,发现 B 尚未创建,所以会执行创建对象 B 的过程。
  3. 在创建对象 B 的过程中,执行实例化(Instantiation)和属性赋值(Populate)操作。此时发现,对象 B 依赖对象 A。
  4. 继续,尝试在缓存中查找对象 A。先查找一级缓存,发现一级缓存中没有对象 A(因为对象 A 还未初始化完成);转而查找二级缓存,二级缓存中也没有对象 A(因为对象 A 还未属性赋值);转而查找三级缓存

singletonFactories,对象 B 可以通过 ObjectFactory.getObject 拿到对象 A。

  1. 继续,对象 B 在获取到对象 A 后,继续执行属性赋值(Populate)和初始化(Initialization)操作。对象 B 完成初始化操作后,会被存放到一级缓存中。
  2. 继续,转到「对象 A 执行属性赋值过程并发现依赖了对象 B」的场景。此时,对象 A 可以从一级缓存中获取到对象 B,所以可以顺利执行属性赋值操作。
  3. 继续,对象 A 执行初始化(Initialization)操作,完成后,会被存放到一级缓存中。

(1)针对非AOP的情况,如果只有一级缓存,则在A刚开始创建的一个初始化不完全的对象放入到一级缓存中就行了,然后B去取的时候直接从一级缓存中去取就可以

(2)针对带AOP的情况,如果只有一级缓存,则在A刚开始实例的话的时候创建的代理对象,而不是原始对象,然后将这个代理对象放入一级缓存中,创建B的时候也是创建代理,然后注入A的代理,再放入一级缓存,然后A就可以取到B的代理

3.3 如果我创建的时候就是需要使用构造方法来注入另外一个bean呢?能理解我的意思嘛?

推荐文章:为什么推荐构造器注入

针对你的问题,我们先回顾下构造器注入与属性注入的区别。

  • 属性注入

:在上文中描述的循环依赖主要是基于属性注入,也就是说对象A的一个属性需要对象B,同时对象B的一个属性又需要对象A。在这种情况下,可以通过三级缓存机制来解决循环依赖的问题,即先实例化A(未完全初始化),然后在实例化B时注入A,最后完全初始化A并注入B。

  • 构造器注入

:如果使用构造器注入的话,对象在构造时就需要注入其依赖,这意味着完全初始化某个对象之前,其依赖的对象也需要完全初始化。如果存在循环依赖的情况,例如对象A的构造器需要对象B,而对象B的构造器又需要对象A,这就形成了一个死循环。在这种情况下,Spring容器会抛出一个异常,表明存在循环依赖。

所以,回答你的问题:

如果我创建的时候就是需要使用构造方法来注入另外一个bean呢?

答:如果你使用构造方法来注入另一个bean,并且存在循环依赖的情况,那么Spring将无法正确地创建这两个bean。你将会收到一个关于循环引用的异常。因此,为了避免这个问题,应该重新设计bean的依赖结构或者使用属性注入而不是构造器注入。

简单说,Spring的三级缓存机制能够很好地处理基于属性的循环依赖,但不能处理基于构造器的循环依赖。

3.3.1 为什么构造器注入不能解决循环依赖问题

当两个类通过构造器注入互相引用时,它们都需要在彼此的构造函数中进行实例化。但在构造一个类的实例之前,必须先构造其依赖的类的实例。这导致了一个问题:无法确定哪个类应该首先被实例化,因为每一个都依赖于另一个已经被实例化。这就是所谓的"循环依赖"问题。

例如,当ServiceA的构造器需要ServiceB的实例,而ServiceB的构造器又需要ServiceA的实例时,这就形成了一个无法解决的循环。在字段或setter注入中,对象可以首先被实例化,然后再设置其依赖关系,但在构造器注入中,这种情况是无法处理的。

3.3.2 如何通过重构代码解决构造器的循环依赖?

重构技巧:新建一个业务类HelperService,将A和B的业务类进行注入,然后在这个新的业务类中调用A和B的相关方法就可以,A和B中同时注入

重构前的代码:

public class ServiceA { private ServiceB serviceB; public void setServiceB(ServiceB serviceB) { this.serviceB = serviceB; } public void someMethodInA() { serviceB.someMethodInB(); } } public class ServiceB { private ServiceA serviceA; public void setServiceA(ServiceA serviceA) { this.serviceA = serviceA; } public void someMethodInB() { serviceA.someMethodInA(); } }

重构后的代码:

public class ServiceA { private HelperService helperService; public void setHelperService(HelperService helperService) { this.helperService = helperService; } public void someMethodInA() { helperService.commonMethod(); } } public class ServiceB { private HelperService helperService; public void setHelperService(HelperService helperService) { this.helperService = helperService; } public void someMethodInB() { helperService.commonMethod(); } } public class HelperService { public void commonMethod() { // Common logic that both ServiceA and ServiceB depend on } }

4 看题解答

class Student{ String name; int age; public Student(String name,int age){ this.name=name; this.age=age; } } Map<Student,Student>mp=new HashMap<>(); Student a= new Student("yxg",1); Student b= new Student("yxg",1) mp.put(a); mp.put(b);

4.1 这里的第二个put会覆盖第一个嘛?

在Java中,HashMap的键的唯一性是基于对象的hashCode()方法和equals()方法来确定的。对于您提供的Student类,您没有重写这两个方法,因此类的默认实现会被使用,这意味着对象的地址会被用来判断其在哈希映射中的唯一性。

因此,即使a和b有相同的name和age属性值,它们在内存中是两个不同的对象。由于它们有不同的地址,因此它们的默认hashCode()值也会不同。结果是,mp.put(a)和mp.put(b)会被视为两个不同的键,所以第二个put不会覆盖第一个。

如果您想使两个具有相同name和age属性值的Student对象被视为相同的键,您需要在Student类中重写hashCode()和equals()方法:

@Override public int hashCode() { return Objects.hash(name, age); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Student student = (Student) obj; return age == student.age && Objects.equals(name, student.name); }

现在,当您再次使用mp.put(a)和mp.put(b),第二个put会覆盖第一个,因为它们现在被视为相同的键。

4.2 如果没有重写equals和hashcode方法,两个方法默认是怎么实现的?

在 Java 的 Object 类中,equals() 和 hashCode() 的默认实现如下:

  1. equals(Object obj) 的默认实现:

public boolean equals(Object obj) { return (this == obj); }

这个默认实现是基于对象的身份进行比较的。即,如果两个对象引用指向的是同一个对象实例(比较的地址),那么 equals() 方法返回 true。否则,返回 false。

  1. hashCode() 的默认实现:

public native int hashCode();

这是一个本地方法,它不是在 Java 中实现的,而是在 JVM 的 C 或 C++ 实现中。但它的大致行为是返回对象的内存地址或与之相关的一个值。重要的是要记住,虽然这个哈希码和对象的内存地址有关,但它并不直接是对象的内存地址。

这就是为什么通常建议:当你重写 equals() 时,也应该重写 hashCode(),以确保两个认为是相等的对象(根据 equals() 方法)也有相同的哈希码。

4.3 怎么重写equals呢

参考4.1中的代码

5 Serializable接口

5.1 这个接口是用来做什么的?

Serializable 是 Java 提供的一个标记接口(即没有任何方法的接口),它告诉 Java 虚拟机 (JVM) 一个类的对象可以被序列化。序列化是将对象的状态信息转化为可以存储或传输的形式的过程。这个形式通常是字节流,但也可以是其他格式。当对象被序列化后,可以将其写入到文件、发送到网络等,然后在适当的时候再反序列化回对象。

序列化主要用于以下场合:

  1. 持久化数据:对象的状态可以被存储在磁盘上,之后再恢复。
  2. 远程方法调用 (RMI):对象可以在网络上进行传输,然后在另一侧反序列化,以便在远程系统上使用。
  3. 分布式计算:对象可以在集群或网格中的不同节点间传输。

简而言之,Serializable 接口使得一个类的对象可以被转换为字节流,从而可以进行存储、传输,并在之后再转换回原始对象状态。

5.2 我们在spring中经常会将对象实例序列化成一个json哈,那这个时候并不需要实现这个接口对吧?

是的,你是对的。在 Spring 或许多其他框架中,当我们将对象实例序列化为 JSON 时,我们通常不需要让该对象实现 Serializable 接口。这是因为 JSON 序列化通常是通过反射来访问对象的字段并将它们转化为 JSON 字符串的,而不是通过 Java 的内置序列化机制。

Serializable 接口主要用于 Java 原生的序列化机制,特别是当你需要将对象状态保存到文件或通过网络在 Java 应用之间发送对象时。这个机制涉及到将对象转化为一个特定的字节流格式,该格式可以随后再反序列化回对象。

但在现代应用中,JSON 已经成为了一种非常流行的数据交换格式,特别是在 web 应用和 RESTful 服务中。为了序列化和反序列化 JSON,我们通常会使用如 Jackson、Gson 等库,这些库并不要求对象实现 Serializable 接口。

5.3 如何使用这个序列化接口

当你想通过Java原生的序列化机制序列化一个对象时,该对象的类必须实现Serializable接口。这是一个标记接口,也就是说它没有任何方法需要实现,但是它告诉JVM这个对象是可以序列化的。

以下是一个简单的Java示例,展示了如何使用Serializable接口进行序列化和反序列化:

java 复制代码
import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

public class SerializationDemo {
    public static void main(String[] args) {
        // 对象序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            Person person = new Person("John", 25);
            oos.writeObject(person);
            System.out.println("Person object has been serialized.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 对象反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("Deserialized Person: " + deserializedPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

注意事项:

  1. serialVersionUID

是一个私有的静态常量,用于表示序列化版本。这是可选的,但建议总是包含它,以确保序列化兼容性。

  1. 如果类的字段发生改变(例如添加新字段),可能需要更改

serialVersionUID。如果你没有设置serialVersionUID并且更改了类的结构,那么在尝试反序列化旧的对象时,可能会收到InvalidClassException。

  1. 不是所有的Java对象都可以被序列化。对象必须是可序列化的,并且它引用的所有对象也都必须是可序列化的。如果对象包含不能序列化的字段,你可以将该字段标记为

transient,这样它就不会被序列化。

  1. 使用ObjectOutputStream来序列化对象,并将其写入文件。使用ObjectInputStream从文件读取并反序列化对象。

5.4 这个接口里有一个id,你知道这个id是干什么的嘛?

它是serialVersionUID, 是一个私有的静态常量,用于表示序列化版本。这是可选的,但建议总是包含它,以确保序列化兼容性。

注意事项:

  1. serialVersionUID

是一个私有的静态常量,用于表示序列化版本。这是可选的,但建议总是包含它,以确保序列化兼容性。

  1. 如果类的字段发生改变(例如添加新字段),可能需要更改

serialVersionUID。如果你没有设置serialVersionUID并且更改了类的结构,那么在尝试反序列化旧的对象时,可能会收到InvalidClassException。

  1. 不是所有的Java对象都可以被序列化。对象必须是可序列化的,并且它引用的所有对象也都必须是可序列化的。如果对象包含不能序列化的字段,你可以将该字段标记为

transient,这样它就不会被序列化。

使用ObjectOutputStream来序列化对象,并将其写入文件。使用ObjectInputStream从文件读取并反序列化对象。

5.4.1 为什么需要这么一个serialVersionUID字段?

答:因为接收方反序列化发送方发送的字节流时,需要有一个对象来对接,从字节流中解析出的所有字段必须在对接的对象在全部存在,否则会造成数据不一致。这里也就是说接收发送双方的序列化和反序列化的对象的版本必须一致。比如发送方在第一次发送Person对象的字节流时,这个对象只有name字段,接收方的对接对象也只有name字段,接收和发送双方使用的对象版本号都是1,所以第一次接收方反序列化成功,随后发送方往Person中新增了一个age字段,并且版本号置为2,但是接收方的对接Person并没有新增这个字段并且版本号还是1,那么发送方第二次发送的字节流,接收方会抛异常。

5.4.2 所以如果发送方新增了一个字段,将serialVersionUID自增,同时也需要将接收端的这个对应接收类添加相应的字段并且自增相应的版本嘛,所以在双方的版本和字段都同步更新的情况下,一定能反序列化成功,对嘛?

是的,你理解得很准确。当你手动管理serialVersionUID时,当发送方的类结构发生变化(例如,添加或删除字段),你需要确保:

更新serialVersionUID。

保证发送方和接收方使用的类定义是同步的。

只要双方的类定义(包括字段和serialVersionUID)保持一致,就可以成功进行序列化和反序列化。

但需要注意的是,仅仅同步serialVersionUID是不够的。例如,如果发送方增加了一个字段并更新了serialVersionUID,而接收方仅更新了serialVersionUID而没有添加相应的字段,那么在反序列化时仍然会出错。

因此,手动管理serialVersionUID和确保类的同步都是为了保证序列化的兼容性。当你有完全的控制权,并可以确保两边都同步更新时,这种方式很有效。但在大型分布式系统中,可能需要更多的策略和工具来管理这种兼容性。

5.4.3 如果接收方和发送方的序列化的原始类能做到同步更新保持一致,这个serialVersionUID应该能够允许去掉把?

理论上,如果你确保两端的类结构始终是一致的,那么serialVersionUID可以省略,因为JVM会为没有定义serialVersionUID的类生成一个默认的版本号。这个默认的版本号是基于类的结构(包括成员变量、方法等)计算出来的。

但实际上,这样做存在风险:

隐式行为:省略serialVersionUID意味着JVM会为你的类生成默认的版本号。当类的任何细节发生变化时,这个版本号也会发生变化。这种隐式的行为可能会导致难以预见的反序列化问题。

跨JVM版本的不确定性:不同的JVM实现(或版本)可能会产生不同的默认serialVersionUID值,这可能会导致在不同的JVM版本之间进行序列化/反序列化时出现问题。

减少出错的机会:手动设置serialVersionUID意味着你已经考虑过版本兼容性问题。当类的结构发生改变时,有意识地更改或保持serialVersionUID值可以减少意外的序列化问题。

所以,即使你能确保发送方和接收方的类结构始终保持一致,为了减少潜在的序列化问题,最佳实践还是推荐为你的可序列化类显式地定义serialVersionUID。

5.5 分布式环境下,一般是如何做到管理这个对象的版本的呢?

在分布式环境下,对象的版本管理变得尤为重要,因为服务间通信、数据交换和状态共享是分布式系统的基础。下面是一些建议和最佳实践,以确保对象版本在分布式环境中得到适当的管理:

  1. 中央化仓库:使用中央化的仓库(如 Maven Central、Nexus、Artifactory 等)来存储和管理所有的JAR包和库。这确保了在分布式环境中,所有服务都引用的是相同的库版本。

  2. 契约驱动的设计 (Contract-Driven Design):在微服务环境中,你可以使用工具(如Spring Cloud Contract)来定义并验证服务间的交互。这确保了服务间的接口和数据格式的一致性,而不需要每个服务都更新到最新版本。

  3. 使用数据模式管理:对于如 Apache Kafka、Apache Avro 这样的系统,你可以使用 Confluent Schema Registry 或 Apache Avro 的内置模式版本控制来管理数据结构的变化。

  4. 向后兼容:尽量使新版本的对象向后兼容,这样即使服务版本不一致,它们仍然可以正常交互。

  5. 版本命名约定:遵循一致的版本命名约定,例如语义版本控制(Semantic Versioning),这样你可以通过版本号轻松地了解更改的性质。

  6. 弃用策略:如果你需要移除或更改对象的某个部分,提供一个过渡期,并在此期间支持旧版本。这给予其他服务足够的时间来进行必要的调整。

  7. 服务发现与注册:使用服务注册与发现机制(如Eureka、Consul等),这样服务可以知道其他服务的版本,并据此做出决策。

  8. 监控与警告:使用监控工具来跟踪分布式环境中的版本变化。如果检测到不一致的版本,立即发出警告。

  9. 灰度部署与金丝雀发布:在引入新版本的服务或对象时,不要立即在所有实例上部署。先在一小部分实例上部署,确保其与其他服务的兼容性,然后再逐渐扩大部署范围。

  10. 维护文档:持续更新文档,记录每个版本的更改和不同版本之间的差异。

在分布式环境中,版本管理是一个持续的、需要多方面关注的过程。与团队合作,制定策略,并使用工具来自动化流程,是确保成功的关键。

6 设计模式

6.1 一般什么设计模式下会用到抽象类,什么会用到接口呢?

1 使用到抽象类的情况:模板模式、工厂模式、组合模式

2 使用到接口的情况:策略模式、工厂模式、观察者模式

设计模式旨在解决特定的设计问题,为此它们经常使用抽象类和接口。下面是一些经常使用抽象类或接口的设计模式的例子:

使用抽象类的设计模式:

  1. 模板方法模式:此模式在一个方法中定义一个算法的骨架,并将一些步骤延迟到子类中。这允许子类在不改变算法结构的情况下重新定义算法的某些步骤。在这个模式中,基本算法的结构被定义在抽象类中的模板方法中。

  2. 工厂方法模式:在这个模式中,一个方法用于创建对象,而实际决定哪个类要被实例化的工作被推迟到子类中。这是通过抽象类和其中的抽象工厂方法实现的。

  3. 策略模式:尽管策略模式经常使用接口,但有时它也可以用抽象类来提供某些默认的实现。

  4. 组合模式:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对待单个对象或组合对象的使用具有一致性。

使用接口的设计模式:

  1. 策略模式:策略模式定义了一系列的算法,并将每一个算法封装起来。这些算法可以相互替换,使算法独立于使用它的客户端。在此模式中,策略通常由一个接口定义,具体策略则实现此接口。

  2. 命令模式:在此模式中,请求被封装为对象,从而允许用户使用不同的请求、队列请求、或记录请求日志,以及支持可撤销的操作。命令通常通过接口定义,并有多个具体的命令实现。

  3. 观察者模式:此模式定义了一个一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会被通知并自动更新。在此模式中,通常使用接口定义观察者和被观察的对象。

  4. 适配器模式:适配器模式将一个类的接口转换成客户端期望的另一个接口。适配器让原本接口不兼容的类可以一起工作。在这种情况下,通常使用目标接口定义期望的输出。

  5. 装饰器模式:这个模式允许动态地向一个对象添加额外的职责。它是对子类化的一个灵活的替代方案。在这个模式中,装饰器通常实现一个与其装饰的组件相同的接口。

总的来说,选择抽象类或接口取决于设计问题和设计模式的具体应用。但两者都是面向对象设计和设计模式中的核心概念。

6.2 策略模式和模板模式如何结合呢?

当然可以。以下是一个简化但更完整的在线订单处理程序,结合了模板模式和策略模式:

订单处理框架

  1. 验证订单
  2. 应用折扣
  3. 支付
  4. 生成收据

我们将允许不同的支付策略,并在某些步骤中为子类提供默认实现,同时允许它们覆盖这些实现。

java 复制代码
// 定义支付策略接口
interface PaymentStrategy {
    void pay(double amount);
}

// 信用卡支付策略实现
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String expiryDate;

    public CreditCardPayment(String cardNumber, String expiryDate) {
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
    }

    @Override
    public void pay(double amount) {
        // 简化为一个输出
        System.out.println("Paid " + amount + " using credit card ending with " + cardNumber.substring(cardNumber.length() - 4));
    }
}

// PayPal支付策略实现
class PaypalPayment implements PaymentStrategy {
    private String email;

    public PaypalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        // 简化为一个输出
        System.out.println("Paid " + amount + " using PayPal with email: " + email);
    }
}

// 订单处理抽象类(模板模式)
abstract class OrderProcessor {
    protected double orderAmount;

    public OrderProcessor(double orderAmount) {
        this.orderAmount = orderAmount;
    }

    // 这是模板方法
    public final void processOrder() {
        validateOrder();
        applyDiscount();
        paymentStrategy.pay(orderAmount);
        generateReceipt();
    }

    // 默认验证实现
    protected void validateOrder() {
        System.out.println("Order validated.");
    }

    // 默认折扣实现
    protected void applyDiscount() {
        // 这里只是一个简单的示例
        orderAmount *= 0.95; 
        System.out.println("Discount applied. New amount: " + orderAmount);
    }

    // 子类可以覆盖的收据生成步骤
    protected abstract void generateReceipt();

    // 使用策略模式的支付步骤
    protected PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }
}

// 一个特定的订单处理实现
class OnlineOrderProcessor extends OrderProcessor {
    public OnlineOrderProcessor(double orderAmount) {
        super(orderAmount);
    }

    @Override
    protected void generateReceipt() {
        System.out.println("Online receipt generated for amount: " + orderAmount);
    }
}

// 主程序
public class Main {
    public static void main(String[] args) {
        OrderProcessor order = new OnlineOrderProcessor(100);
        order.setPaymentStrategy(new CreditCardPayment("1234567812345678", "12/25"));
        order.processOrder();

        System.out.println("-----------");

        OrderProcessor anotherOrder = new OnlineOrderProcessor(150);
        anotherOrder.setPaymentStrategy(new PaypalPayment("user@example.com"));
        anotherOrder.processOrder();
    }
}

在这个示例中,我们使用了模板模式来定义订单处理的基本步骤,并使用策略模式为支付步骤提供不同的策略。

6.3 你还看过其他的设计模式嘛?

观察者模式

6.4 讲讲观察者模式

观察者模式:https://www.runoob.com/design-pattern/observer-pattern.html

在epoll中,可以将socket缓冲区看作是主题,而epoll线程(或epoll机制中用来监视事件的部分)作为观察者。

让我们再次澄清这个概念:

epoll与观察者模式的关系

  • 被观察对象 (Subject): 文件描述符相关的缓冲区、状态等。例如,一个TCP套接字的接收缓冲区。

  • 观察者 (Observer) : epoll机制。它注册了对特定文件描述符的关注,并等待这些文件描述符上的特定事件(如可读、可写等)。

当一个文件描述符变得可读或可写时(例如,因为有新数据到达了接收缓冲区),缓冲区/状态(作为被观察对象)发生了变化。epoll(作为观察者)会被通知这个变化,然后相应的处理代码就会被调用。

所以,你是对的,epoll更像是观察者,而文件描述符的状态和缓冲区才是被观察的对象。再次为之前的误解道歉,感谢你指出。

6.4.1 观察者模式在java中的哪些框架中得到了应用呢?

观察者模式是一个相当通用的设计模式,并在多种Java框架和库中得到了应用。以下是其中的一些示例:

  1. JavaBeans: JavaBeans 使用属性更改监听器来通知其他对象属性的更改。这实质上是观察者模式的一个变种,其中属性更改监听器扮演观察者的角色,JavaBean 是被观察对象。

  2. Swing: Swing GUI库中的大多数组件(如按钮、文本框等)都支持事件监听。当用户与这些组件交互(例如点击按钮)时,将触发事件,注册的监听器(观察者)将接收到通知。

  3. JavaFX: 与Swing类似,JavaFX也使用了观察者模式来处理用户界面事件。例如,当用户与按钮或其他控件交互时,可以注册事件处理程序来响应。

  4. RxJava: RxJava是一个响应式编程库,它在很大程度上建立在观察者模式之上。在这里,Observable对象表示一个数据流,Observer则订阅这个数据流并响应数据项、错误或流的完成。

  5. Spring Framework : 在Spring中,可以使用@EventListener注解来监听和处理应用程序事件 。这也是观察者模式的应用,其中事件发布者是被观察者,而使用@EventListener注解的方法是观察者。

  6. Java 9的Flow API : Java 9引入了Flow API作为响应式流的一部分,其中Publisher代表数据源(被观察者),而Subscriber则代表数据消费者(观察者)。

这只是观察者模式在Java世界中应用的一些示例,实际上它在许多其他场景和库中也得到了广泛使用。

6.4.2 发布订阅机制是一种观察者模式的实现吗

是的,发布-订阅(Pub-Sub)机制是观察者模式的一种变种或特定实现。它们都涉及到一种"一对多"的依赖关系,其中多个对象依赖于一个对象的状态变化。当该对象状态发生变化时,依赖它的对象会被通知。

具体来说,两者之间的主要差异和特点如下:

  1. 通信方式:

    • 观察者模式: 通常直接涉及到具体的观察者和被观察者对象。当被观察者的状态变化时,它直接通知注册的观察者。
    • 发布-订阅模式: 通常涉及到一个中间组件(经常是消息代理或事件总线),负责管理订阅和发布消息。发布者发布消息,但并不直接发送给订阅者。相反,发布的消息被放入一个中心位置,并由订阅者从那里获取或被动地接收。
  2. 耦合:

    • 观察者模式: 被观察者通常需要知道其观察者的某些信息,这意味着它们之间存在直接的关联。
    • 发布-订阅模式: 由于存在中间组件,发布者和订阅者之间的耦合度较低,它们可能根本不知道彼此的存在。
  3. 使用场景:

    • 观察者模式: 通常用于单一应用程序内的组件或对象之间的交互。
    • 发布-订阅模式: 经常用于分布式系统,其中生产者和消费者可能位于不同的系统或网络节点上。
  4. 消息传递:

    • 观察者模式: 通常是同步的,即观察者响应状态的改变是即时的。
    • 发布-订阅模式: 可以是同步的也可以是异步的,取决于实现和配置。

总的来说,发布-订阅是观察者模式的扩展或特殊形式,具有更高的灵活性,尤其适用于大型、分布式系统。

7 场景设计题

比如说现在有一个业务场景是主线程开启多个子线程去分别调用一个第三方接口,然后子线程需要将方法的返回值返回给主线程,主线程来去处理,处理完后再返回给用户;但是第三方的接口不稳定,有可能出现超时或者出现异常,比如在子线程里抛出一个Exception出来,我想在主线程里对这个异常做处理,或者是子线程对主线程做异常通知,相当于是主线程知道哪一个抛出了异常

java里面,主线程如何捕获子线程的异常并且进行异常处理

7.1 我的解决方案

利用观察者模式,每一个子线程都添加这个主线程当作观察者,当某一个子线程抛出异常的时候,就调用自己存储的观察者回调函数,通知主线程进行异常处理

7.2 你提出的方案不是java倡导的啊,jdk本身就提供了相关的能力,你能讲讲嘛

当然可以!

在Java中,主线程捕获子线程异常的情境可以采用多种方案。观察者模式是一种可行的方案,但确实,Java为此提供了更原生和直接的方法。特别是,FutureExecutorService是处理这种情况的非常方便和推荐的工具。

7.2.1 FutureExecutorService

Future代表了一个异步计算的结果,而ExecutorService是一个线程池服务,用于并发地执行任务。

java 复制代码
ExecutorService executor = Executors.newFixedThreadPool(5);  // 例如,创建一个包含5个线程的固定线程池。

List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    Callable<String> task = () -> {
        if (new Random().nextBoolean()) {
            throw new RuntimeException("Error in child thread");
        }
        return "Task completed";
    };
    futures.add(executor.submit(task));
}

for (Future<String> future : futures) {
    try {
        String result = future.get();  // 如果子线程抛出了异常,它将会在这里被捕获。
        System.out.println(result);
    } catch (ExecutionException e) {
        Throwable cause = e.getCause();
        System.out.println("Exception from child thread: " + cause.getMessage());
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        System.out.println("Main thread was interrupted");
    }
}

executor.shutdown();

7.2.2 Thread.setUncaughtExceptionHandler

另一个方法是使用setUncaughtExceptionHandler,这是每个Thread对象都有的一个方法。它允许您为线程指定一个处理程序,当线程抛出一个未捕获的异常时,这个处理程序就会被调用。

java 复制代码
Thread thread = new Thread(() -> {
    throw new RuntimeException("Error in child thread");
});

thread.setUncaughtExceptionHandler((t, e) -> {
    System.out.println("Exception from thread " + t.getName() + ": " + e.getMessage());
});

thread.start();

但是,请注意,与Future相比,此方法不允许您获取线程的返回值,而只是处理异常。

总之,FutureExecutorService更适合大多数并发任务,因为它们提供了更好的控制和更大的灵活性。

7.3 java的FutureTask这种捕获异常的方式运用了什么模式,是观察者模式嘛

这种通过FutureFutureTask来捕获异常的方式并不是基于观察者模式。它更多地依赖于任务执行的封装和内部状态管理。

但让我们对比一下这两者以便更好地理解:

1. Future/FutureTask的异常捕获:

  • 封装与内部状态 :当你提交一个CallableExecutorService执行,实际上是将该Callable封装在FutureTask里。FutureTask内部维护了一个状态,例如是否已完成、是否已取消,以及结果或异常。

  • 状态变更 :当Callable执行完成,无论是正常完成还是抛出异常,FutureTask的内部状态都会被更新。

  • 结果或异常的查询 :当你调用future.get()时,根据FutureTask的内部状态来返回结果或抛出异常。

2. 观察者模式:

  • 主题和观察者:观察者模式涉及到主题(Subject)和观察者(Observer)。当主题的状态发生变化时,它会通知所有的观察者。

  • 动态注册:观察者可以在运行时动态注册或取消注册于某个主题。

  • 推或拉:当状态变更时,主题可以选择"推"变更给观察者,或者观察者可以"拉"变更。

对比这两者,我们可以看到Future的工作方式主要是通过封装和状态管理,而不是像观察者模式那样基于通知和动态注册。

然而,CompletableFuture的某些功能(如回调和组合)与观察者模式的概念更为接近,因为你可以注册回调函数以响应CompletableFuture的完成事件,但这也不是传统的观察者模式。

7.4 Future,FutureTask,CompletableFuture之间的区别

  1. Future, FutureTask, 和 CompletableFuture之间的区别:

    • Future:

      • Future是Java并发编程的基础接口,它代表了一个异步计算的结果。它有几个基础的方法,如get(), isDone(), 和 cancel()
      • 通常,你不会直接实例化一个Future对象,而是通过如ExecutorServicesubmit()方法获得。
    • FutureTask:

      • FutureTaskFuture接口的一个实现,并且同时实现了Runnable接口,所以它可以被执行(例如在一个线程或线程池中)。

      • FutureTask是一个包装器,它接收一个Callable对象并在内部执行它,之后你可以从FutureTask对象中获取结果。

      • 示例:

        java 复制代码
        Callable<Integer> callable = () -> { /*... some computation ...*/ return 42; };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
    • CompletableFuture:

      • CompletableFutureFuture的扩展,提供了更加丰富和强大的异步编程能力。

      • 它支持任务完成后的回调、任务组合、任务链式调用等,使你能够更容易地构建异步的工作流。

      • 还提供了与流式API结合使用的能力。

      • 示例:

        java 复制代码
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 42)
                                                            .thenApply(r -> r * 2);

    7.5 为什么调用future.get()能捕获异常?深入的讲一下jvm层面java是怎么捕获异常的,应用层面呢

    • JVM层面 :
      • 当你提交一个Callable任务给ExecutorService,它将在一个线程中执行。如果该Callable抛出异常,该异常会在执行它的线程中被捕获,并被存储。
      • 当你调用future.get()时,你正在尝试获取Callable的结果。但是,如果在计算期间抛出了异常,get()方法会从存储的状态中重新抛出该异常,封装在ExecutionException中。因此,你在调用get()方法时捕获到了原来在Callable中发生的异常。
    • 应用层面 :
      • Future的设计意图之一就是能够保存和传递执行期间发生的异常。通过Future的设计,使你能够在一个线程中执行任务并在另一个线程中检索结果或处理异常,无论它们是否成功。
      • Callable抛出异常时,这些异常不会直接影响到调用ExecutorService.submit()的线程。而是,异常被封装并存储,等待get()方法的调用者来处理。

这种异常处理方式的优势在于,它使你能够集中处理所有子线程的异常,而不必在每个子线程中分散处理异常逻辑。

8 线程池

8.1 线程池的核心线程数和最大线程数应该怎么样确定?

答:

cpu密集型应用:两个参数一样,线程数一般是cpu核数+1,多出来的一个线程负责调度。

IO密集型应用:最大线程数会比核心线程数大很多,是因为涉及到磁盘或者内存IO,cpu的利用率受限

8.2 如果最大线程数也占满的情况下,队列中的任务很容易超时,这时队列的存在不就没有意义了嘛,怎么办?

答:

当最大线程数都已经被占满,并且队列中的任务频繁超时,那么确实存在一种资源过载的问题。队列的主要意义是为了缓冲请求,当并发请求超过线程池的处理能力时,新的任务可以暂存在队列中等待执行,而不是立即被拒绝。

但是,如果任务在队列中等待的时间太长,以至于当它得到执行时已经超时,那么队列的存在确实会带来问题。此时,需要重新评估和优化以下几个方面:

  1. 调整线程池的大小:根据系统的实际需求和资源利用情况,可能需要增加线程池的最大线程数。

  2. 调整队列的大小:如果队列经常满,而且任务的执行时间不长,那么可以考虑增加队列的大小。

  3. 优化任务执行时间:如果队列中的任务执行时间过长,考虑对任务进行优化,使其执行更快。

  4. 限流策略:在面对突然的高并发请求时,可以通过一些限流技术(如令牌桶、漏斗)来限制进入系统的请求速率,保证系统的稳定性。

8.3 有哪一种队列解决这种问题嘛?(面试官想表达的意思是看具体的需求应用相关的策略)

答:

如果线程池和队列都满了,对于新的任务,一种比较好的队列策略是使用"拒绝策略"(Rejection policy)。Java的ThreadPoolExecutor提供了几种内置的拒绝策略,包括抛出异常、放弃、直接在调用者线程中执行等。

但对于问题的核心------如何确保队列中的任务不超时,答案可能更多地依赖于应用的具体需求和环境,而不仅仅是队列的类型。不过,以下是一个可能的策略:

(1)定时任务,使用ScheduledThreadPoolExecutor: 这是一个特殊的线程池,可以用来处理延迟任务或定时任务。你可以为每个任务设置一个超时时间。当任务的等待时间超过这个值时,可以考虑将其从队列中移除。

此外,如果你的应用需要一种能够自动调整大小的队列,可以考虑实现自己的队列逻辑,但这通常需要深入的技术知识和对系统需求的深入了解。

(2)对于大量短生命的任务,使用无界队列也可以,因为任务很快就能执行完,队列中的任务也会很快执行。

9 数据库

sql 复制代码
给出两张表t1和t2,有两条sql语句:t2上一个外键是t1的主键

t1

id

1

2

3

t2:

id fid

1 1

2 2

9.1 select t1.id from t1 inner join t2 on t1.id on t2.fid 会产生几条数据

答:2

9.2 select t1.id from t1 left join t2 on t1.id on t2.fid 会产生几条数据

答:3

9.3 如果我把9.2中的on条件删除了,变成select t1.id from t1 inner join t2;它还会执行吗?

答:会,返回的是笛卡尔积

9.4 索引问题

为user表的(a,b,c)字段建立组合索引,下面哪几个查询会用到

select * from user where a = 1 and  b=2 and c>1

select * from user where b=2 and c>1

select * from user where c=2 and a = 1 

在没有优化器执行顺序的情况下,

以上第一个的a=1和b=2都会使用到索引,第三个的a=1会使用到索引,但是第二个无论是否使用优化器都不会走索引

9.4.1 为什么第三个sql语句使用了索引

对于组合索引(a,b,c),其前缀有(a),(a,b),(a,b,c),那么第三个sql的a=1刚好符合这一个前缀条件,这是因为where语句中的前缀匹配不是从第一个条件开始就一定要按照最左匹配原则的顺序和位置来的,而是允许在and的条件下中的某一个组合符合最左前缀匹配;

但是也有可能因为索引优化器的存在,将查询条件重拍成了能走联合索引的形式

9.4.2 这和索引下推有关系吗

是的,你提到的索引下推(Index Condition Pushdown,简称 ICP)是数据库优化的一种策略,特别是在MySQL 5.6及以后的版本中被引入。但它与我们之前讨论的最左前缀原则略有不同,让我们详细了解一下。

索引下推(Index Condition Pushdown,ICP):

ICP是一种优化策略,它允许数据库在检索数据之前就在索引层面上评估WHERE子句的某些部分。传统上,数据库首先使用索引检索数据,然后在检索到的数据上应用WHERE子句的过滤条件。通过ICP,一部分过滤可以在使用索引时就被应用,这样可以减少从存储引擎检索的数据量。

如何与最左前缀原则关联:

考虑一个组合索引 (a, b, c),假如我们有查询:

sql 复制代码
select * from user where a=1 and b>2 and c=3;

在没有ICP的情况下,数据库首先使用索引的 a 部分来检索数据,然后在结果上应用 bc 的条件。

但是如果使用了ICP,当数据库在索引上检索 a=1 时,它还可以同时考虑 b>2 的条件,从而减少需要检索的数据量。

然而,请注意,即使有了ICP,如果查询不遵循最左前缀原则(例如,查询只有 bc 的条件),那么数据库可能不会有效地使用索引。

结论:

虽然ICP和最左前缀原则都与如何有效地使用组合索引有关,但它们解决的问题是不同的。最左前缀原则关注的是查询条件和索引顺序的匹配,而ICP关注的是如何在检索数据时更早地应用过滤条件。两者都是现代数据库为提高查询性能而采用的策略。

9.5 可重复读和读已提交有什么差别呢?

提交读每一次读都会生成一次最新的快照

可重复读使用的第一次的快照

10 非技术问题

10.1 优缺点

优点:能吃苦,正确的时机做正确的事情,把握住机会,心态比较好

缺点:太乐观了,整天乐呵呵的,没有什么危机感

10.2 未来三到五年后,你希望在团队中担任一个什么样的角色

答:最起码是leader吧

10.3 什么样的leader,管理方面的吗?

答:三年技术leader,五年进管理层

10.4 你希望五年以后做偏管理的工作吗?

答:目前肯定是想做技术,但是后面看机遇吧

10.5 在学校有担任过什么职务吗,或者管理相关的工作吗?

答:本科的时候担任过IT协会的会长

10.6 你在做管理岗位的时候,每天的工作内容是什么呢

答:分享新的技术,好的岗位

10.7 比如你的老板给你安排了一个比较大的专项任务,这个也比较重要,时间比较紧,任务也比较重,人也不多,就那么几个人,需要所有的人的加班才能完成,你怎么去能够激励大家,保证任务按时完成,你会用到哪些方法?

答:

(1)首先可以在技术上进行拆分,有一些项目是由多个子项目完成的,那么是不是可以采用分布式微服务架构,依据团队中成员擅长的点,有的人会做前端,就让他写页面,有的人在做应对大流量高并发上有经验,有的人可能在系统安全和鉴权上比较厉害,给他们分配适合它们专长的一个子项目,每一个子项目形成一个闭环,这样的,各个项目的交互调用api就很方便。

(2)定期汇报,看看这周的OKR完成情况怎么样

10.8 比如你的领导,给你安排了一项工作,可能比较枯燥,比如填一些汇报材料,整理周报,这些不在你本职工作中,这些工作是周期性的执行的,你怎么面对呢?

答:写周报很正常的啊,了解这一周自己干了什么,也是对自己这一周的反思,有没有进步,同时周报本身也花不了多长时间

11 反问

11.1 你们是先统一面试,然后再分配到各个部门呢,还是说谁面的,面试者就进这个面试官的部门?

答:如果全流程通过了,大概率是到我们部门,但是这东西说不准,可能会根据公司的安排,看别的部门缺人与否

11.2 应该还有一轮面试吧?

答:对,然后还有我们的一级部分经理面(总裁),再就是hr面这样的

11.3 我这次表现如何?

答:我觉得你还是不错的,挺好的,我这边是OK的

11.4 用友主要是负责哪一方面的业务

答:以前是做财务软件和ERP比较有名的哈,现在的话用友也在转型,我们现在主要在做自己的一个saas服务平台,叫yongBIP,这个平台主要是在公有云,然后给企业赋能,我们主要是做这个项目下的大财税下的税务领域,比如饭店吃饭,开发票,可能需要一个后台系统把这个票务系统,门店系统,税务局的设备给整合起来打同,然后可以开发票将其发送到消费者邮箱里;一方面是开票,另一方面可能是报票,需要做OCR的时候,根据票面信息生成报价单,跟2c比较相近

还有一个是帮助企业进行税务核算,比如每隔半年或者一年做一次纳税申报,比如我需要缴纳多少税,已经缴了多少税,我能够享受哪些优惠政策,我们需要帮助企业一键申报的;这是很复杂的,因为全国有18种税,每个省的每个税有不一样,另外一块呢是我们拿到数据后,这个数据量又很大,所以我们又可能帮企业做一个观测,比如明年能享受哪些优惠政策啊,哪些东西需要调整,相当于做一个风控,与大数据分析相关。

技术栈用到的springboot,springcloud,es,mysql,mongodb这些都用。

除了这些,我们得支持多种数据库,除了mysql,还有pg,hbase这些,还有一些国产的中间件,也需要支持。

我们也会做一些自研的中间件,比如特制的rpc框架,比如一些存储中间件,兼容不同的数据库,所以我们做了一套自己的吃就层框架,还有元数据信息的框架

11.5 你们部门的话,平常用什么东西比较多?

答:我们部门最早用到的是springcloud,还是用netfix那一套,公司主流的还是想统一技术栈,消息队列用的kafka,数据库用的mysql,但是要兼容其他的数据库,es主要的场景是搜索,比如我们可能会用到一些非结构化的数据;然后还有常用的redis;后来我们还考虑过clickhouse,但是对于客户的部署成本太高了,所以就搁置了

我们用友也有自己的一套上线流程,包括流水线,自动部署,包括开发、部署、测试、上线等流程

面试官:非常希望能和你在一个部门奋斗哈

相关推荐
lapiii3583 分钟前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
程序员小明z6 分钟前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设
爱敲代码的小冰25 分钟前
spring boot 请求
java·spring boot·后端
Lyqfor38 分钟前
云原生学习
java·分布式·学习·阿里云·云原生
我是哈哈hh40 分钟前
HTML5和CSS3的进阶_HTML5和CSS3的新增特性
开发语言·前端·css·html·css3·html5·web
程序猿麦小七1 小时前
今天给在家介绍一篇基于jsp的旅游网站设计与实现
java·源码·旅游·景区·酒店
Dontla1 小时前
Rust泛型系统类型推导原理(Rust类型推导、泛型类型推导、泛型推导)为什么在某些情况必须手动添加泛型特征约束?(泛型trait约束)
开发语言·算法·rust
张某布响丸辣1 小时前
SQL中的时间类型:深入解析与应用
java·数据库·sql·mysql·oracle
喜欢打篮球的普通人1 小时前
rust模式和匹配
java·算法·rust
java小吕布2 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法