LOADING

加载过慢请开启缓存 浏览器默认开启

2024金蝶后端一面

介绍一下自己

面向对象的概念

面向对象是一种编程方式,它将数据及其对数据的操作方法放在一起,作为一个相互依存的整体,这种方式需要将同类对象抽象出其共性,形成类的概念,类中的大部分数据只能用类中的方法进行处理,类通过接口与外界进行交互,这样就可以将复杂的问题简单化,从而提高开发效率。

封装,继承,多态

封装:指的是将数据及其对数据的操作方法放在一起,作为一个相互依存的整体,这种方式需要将同类对象抽象出其共性,形成类的概念,类中的大部分数据只能用类中的方法进行处理,类通过接口与外界进行交互,这样就可以将复杂的问题简单化,从而提高开发效率。

继承:指的是子类可以继承父类的属性和方法,子类可以重写父类的方法,从而实现多态。

多态:同一个方法可以根据对象类型的不同有着不同的实现方式,这种特性称为多态。

java如何实现多态

  1. 方法重写:子类可以重写父类的方法,从而实现多态
  2. 接口实现:一个类实现某个接口,然后通过接口的引用指向该类的实例,从而实现多态
  3. 抽象类:一个抽象类可以有多个子类,子类可以重写父类的方法,从而实现多态

Java基本数据类型和引用数据类型

基本数据类型:byte、short、int、long、float、double、char、boolean
其对应的引用类型:Byte、Short、Integer、Long、Float、Double、Character、Boolean

基本数据类型是直接存储在栈中的,而引用数据类型是存储在堆中的,栈中存储的是引用数据类型的地址,通过地址可以找到堆中的数据

什么情况下使用基本数据类型,什么情况下使用引用数据类型

基本数据类型(byte、short、int、long、float、double、boolean、char)适用于以下情况:

  1. 需要节省内存空间:基本数据类型在内存中占用的空间较小,适合用于大量数据的存储和计算。
  2. 需要高效的计算:基本数据类型的运算速度通常比包装类更快。
  3. 不需要进行空值检查:基本数据类型不能表示空值,如果需要表示空值,必须使用包装类的对象。

包装类(Byte、Short、Integer、Long、Float、Double、Boolean、Character)适用于以下情况:

  1. 需要使用对象的特性和方法:包装类提供了许多有用的方法和属性,可以方便地操作数据。
  2. 需要进行空值检查:包装类的对象可以表示空值,可以使用null来表示缺少数值的情况。
  3. 需要与其他数据类型进行转换:包装类提供了与基本数据类型之间的自动装箱和拆箱功能,方便进行类型转换。

总的来说,基本数据类型适用于需要高效的计算和节省内存空间的场景,而包装类适用于需要使用对象特性和方法、进行空值检查以及与其他数据类型进行转换的场景。

String为何是引用数据类型

因为String类型的变量并不直接存储字符串,而是存储字符串的地址,通过地址可以找到字符串的值,所以String类型的变量是引用数据类型,当我们声明了一个String类型的变量时,实际上是在栈中存储了一个地址,然后在堆中存储了字符串的值,通过地址可以找到堆中的字符串的值

在Java中String类是不可变的,也就是说一旦一个string对象被创建出来了,它的内容就不能被改变,但是String对象本身是可以被改变的,也就是说String对象的引用可以改变,但是String对象的值是不能改变的,这也是为什么String是不可变的

字符串拼接的方式

  1. 使用+号进行拼接
  2. 使用StringBuilder的append方法进行拼接

其中使用+号进行拼接的方式会创建很多的StringBuilder对象,而使用StringBuilder的append方法进行拼接的方式只会创建一个StringBuilder对象,所以使用StringBuilder的append方法进行拼接的方式效率更高
但是在JDK9之后,使用+号进行拼接则不会创建很多的StringBuilder对象,因为改用了动态方法makeConcatWithConstants,这个方法会在编译期间进行优化,所以使用+号进行拼接的方式效率更高

接口和抽象类的区别

  1. 一个类只能继承一个抽象类,但是可以实现多个接口
  2. 接口中所有的默认方法都是public的,而抽象类中的抽象方法可以有多种访问权限
  3. 接口中所有的方法都是抽象方法,而抽象类中可以有非抽象方法
  4. 接口中不可以声明和使用字段,但是可以创建的常量,而抽象类中可以声明和使用字段
  5. 接口中的成员变量只能是public static final类型的,而抽象类中的成员变量可以是任意类型的
  6. 抽象类是对整个类整体进行抽象,包括属性,行为,但是接口却是对类局部(行为)进行抽象,继承一般是指是不是的关系,而接口一般是指有没有的关系
  7. 继承抽象类的是具有相似特点的类,而实现接口的却可以是不相关的类

什么时候使用接口,什么时候使用抽象类

在面向对象编程中,接口和抽象类都是用来定义类的行为和属性的工具。

  1. 当需要定义一组规范或契约,并且这些规范可以被多个类实现时,应该使用接口。接口只定义了方法的签名,而不包含任何实现代码。类可以实现一个或多个接口,从而保证了类的一致性和可替换性。接口的一个典型应用场景是多态,通过接口可以实现多个类的统一调用。

  2. 当需要定义一个通用的基类,并且这个基类包含一些默认的实现代码时,应该使用抽象类。抽象类可以包含抽象方法和具体方法,抽象方法只定义了方法的签名,而具体方法包含了实现代码。子类必须实现抽象方法,但可以选择性地覆盖具体方法。抽象类的一个典型应用场景是模板方法模式,通过抽象类可以定义一个算法的框架,具体的实现由子类提供。

总的来说,接口适用于定义规范和多态,而抽象类适用于定义通用的基类和模板方法。选择使用接口还是抽象类取决于具体的需求和设计目标。

线程的创建方式

  1. 继承Thread类
  2. 使用Runnable接口
  3. 使用Callable接口和FutureTask类
  4. 使用线程池

集合中包含哪些数据结构

ArrayList和LinkedList的区别

  1. ArrayList是基于数组实现的,LinkedList是基于链表实现的

HashMap中的put方法的实现原理

  1. 首先,根据传入的key对象的hashCode()方法计算出哈希值。
  2. 然后,根据哈希值计算出在数组中的索引位置。
  3. 如果该索引位置上的元素为null,表示该位置为空,直接将键值对存储在该位置上,并返回null。
  4. 如果该索引位置上的元素不为空,表示该位置上已经存在其他键值对。
    • 如果该位置上的元素的key与传入的key对象相等(使用equals()方法进行比较),则更新该位置上的值,并返回旧的值。
    • 如果该位置上的元素的key与传入的key对象不相等,则发生了哈希碰撞,需要进行解决。
  5. 解决哈希碰撞的方式是使用链表或红黑树来存储具有相同哈希值的键值对。
    • 如果该位置上的元素是链表,将键值对添加到链表的末尾,并返回null。
    • 如果该位置上的元素是红黑树,调用红黑树的put方法将键值对添加到红黑树中,并返回null。
    • 如果链表的长度达到了阈值(默认为8),将链表转换为红黑树。
  6. 如果添加键值对后,HashMap中键值对的数量超过了负载因子乘以数组的长度(默认为0.75倍),则进行扩容操作。
    • 创建一个新的数组,长度是原数组的两倍。
    • 将原数组中的键值对重新计算索引位置,并存储到新数组中。
    • 将新数组设置为HashMap的数组,并更新相关的属性。
  7. 返回null表示添加成功。

谈谈你对多态的理解

多态是指同一个方法可以根据对象的类型的不同有着不同的实现方式,这种特性称为多态。

spring和springboot的关系,二者启动方式有何区别

spring是一个开源的Java开发框架,它提供了许多的功能,包括IOC容器,AOP,事务管理,MVC框架等,springboot是在spring的基础上进行了封装,使得我们可以更加方便的使用spring,springboot的启动方式是通过main方法启动,而spring的启动方式是通过web.xml文件启动

SpringBoot自动装配原理?

Springboot自动装配的核心思想是:通过读取并解析项目的配置信息,然后根据配置信息自动的将需要的组件注入到Spring容器中,从而实现自动装配的功能。springboot在启动的时候会扫描外部引用jar包中的META-INF/spring.factories文件,然后根据spring.factories文件中的配置信息进行自动装配。

实现自动装配的步骤如下:
在启动类的main方法中会调用SpringApplication.run()方法,该方法的主要作用是去加载启动类。这一机制主要是通过@SpringBootApplication注解实现的,该注解是一个组合注解,包含了@ComponentScan、@EnableAutoConfiguration和@SpringBootConfiguration三个注解,其中@ComponentScan注解的作用是扫描启动类所在包及其子包下的所有类,将它们注入到Spring容器中,@EnableAutoConfiguration注解的作用是启用自动配置,@SpringBootConfiguration注解的作用是将启动类标记为配置类,从而将启动类注入到Spring容器中。

谈一谈你对Spring AOP的理解

AOP是面向切面编程,它是一种编程思想,它的核心思想是将程序中的业务逻辑和系统服务分离,将系统服务抽象为切面,然后将切面织入到业务逻辑中,从而实现业务逻辑和系统服务的分离。

在实现上面spring aop是基于动态代理和字节码操作的,在编译阶段,利用AspectJ编译器将含有切面代码的类编译成字节码文件,然后在运行阶段,spring会根据需要生成Java动态代理或者CGLIB动态代理,然后将切面织入到业务逻辑中,从而实现业务逻辑和系统服务的分离。

具体来说aop的代理invoke方法是通过拦截器的执行来实现的

mysql的锁有哪几种

MySQL的锁可以分为以下几种:

  1. 行级锁:MySQL支持行级锁,即对某一行数据进行锁定,其他事务不能修改该行数据,以保证数据的一致性和完整性。

  2. 表级锁:MySQL也支持表级锁,即对整个表进行锁定,其他事务不能对该表进行任何操作,包括读取和修改。

  3. 共享锁:共享锁是一种读锁,多个事务可以同时持有共享锁,但是不能同时持有排他锁。

  4. 排他锁:排他锁是一种写锁,只有一个事务可以持有排他锁,其他事务不能同时持有共享锁或排他锁。

  5. 意向锁:意向锁是一种辅助锁,用于协调行级锁和表级锁之间的关系,以提高并发性能。意向锁分为意向共享锁和意向排他锁。

  6. 自增锁:自增锁是一种特殊的锁,用于保证自增字段的唯一性。在插入新数据时,MySQL会自动加上自增锁,防止其他事务同时插入相同的值。

索引机制

MySQL的索引机制是一种用于加快数据检索速度的数据结构。MySQL支持多种类型的索引,包括B-tree索引、哈希索引、全文索引等。

B-tree索引是MySQL中最常用的索引类型。它使用B-tree数据结构来存储索引数据,可以快速定位到满足查询条件的数据。B-tree索引适用于范围查询和排序操作。

哈希索引使用哈希函数将索引值转换成哈希码,并将哈希码存储在索引中。哈希索引适用于等值查询,但不支持范围查询和排序操作。

全文索引是一种用于全文搜索的索引类型。它可以将文本数据划分成若干个词条,并为每个词条建立索引。全文索引适用于对文本内容进行搜索的场景。

MySQL的索引可以由用户手动创建,也可以由MySQL自动创建。用户可以根据查询需求和数据特点选择合适的索引类型。同时,用户还可以通过分析查询执行计划和优化器的建议来优化索引的使用。

redis持久化

redis的持久化有两种方式,一种是RDB,一种是AOF

redis如何实现主从复制

Redis通过主从复制来实现数据的复制和备份,主要包括以下几个步骤:

  1. 主节点(Master)将所有写操作的命令记录到内存中的命令缓冲区(Command Buffer)中,然后将这些写命令发送给从节点(Slave)。

  2. 从节点接收到主节点发送的写命令后,将这些命令保存到自己的内存中,并执行这些命令,使得从节点的数据与主节点保持同步。

  3. 从节点周期性地向主节点发送SYNC命令,请求进行全量复制。主节点收到SYNC命令后,会执行BGSAVE命令,将当前内存中的数据快照保存到磁盘上的RDB文件中,并将该文件发送给从节点。

  4. 从节点接收到主节点发送的RDB文件后,会加载该文件恢复数据,并执行主节点在发送RDB文件期间的写命令,保证从节点与主节点的数据一致性。

  5. 从节点在完成全量复制后,会通过命令缓冲区接收主节点发送的增量复制命令,保持与主节点的数据同步。

  6. 当主节点出现故障或者网络异常时,从节点会自动切换为主节点,继续提供读写服务。

需要注意的是,Redis的主从复制是异步的,从节点与主节点之间存在一定的延迟。此外,主从复制还支持多级复制,即从节点可以作为其他从节点的主节点,形成复杂的主从复制拓扑结构。

RabbitMQ怎么解决分布式事务问题,保证消息的一致性

RabbitMQ本身不提供分布式事务的解决方案,但可以通过以下方法来保证消息的一致性:

  1. 发布者确认(Publisher Confirms):发布者在发送消息后等待RabbitMQ的确认,确保消息已经被正确接收和处理。如果RabbitMQ无法处理消息,会返回一个确认,发布者可以根据确认状态进行相应的处理。

  2. 事务机制(Transactional):RabbitMQ支持事务机制,可以将一系列的操作包装在一个事务中,要么全部成功执行,要么全部回滚。通过开启事务模式,发布者可以将多个消息发送操作放在同一个事务中,保证消息的原子性。

  3. 消费者确认(Consumer Acknowledgements):消费者在接收并处理消息后,可以向RabbitMQ发送确认消息,告知RabbitMQ该消息已经被正确处理。如果消费者无法处理消息,可以选择拒绝消息或将消息重新放回队列中,以便其他消费者重新处理。

  4. 消费者幂等性(Consumer Idempotence):消费者需要设计幂等性的处理逻辑,即处理相同消息多次和处理一次的效果是一样的。这样即使消息重复消费,也不会引起数据的不一致性。

通过以上方法的组合应用,可以在一定程度上保证RabbitMQ消息的一致性。但对于分布式事务的需求更为复杂的场景,可能需要借助其他技术或框架来解决,如使用分布式事务管理器(如Atomikos、Bitronix)或基于消息的最终一致性方案(如TCC、Saga模式)。

nginx默认的负载均衡算法,如何实现动静分离

Nginx默认的负载均衡策略是轮询(Round Robin)策略。在轮询策略下,Nginx会按照请求的顺序依次将请求分发到不同的后端服务器上,实现请求的负载均衡。

要实现动静分离,可以通过Nginx的配置文件来实现。具体步骤如下:

  1. 配置静态文件目录:在Nginx的配置文件中,通过root指令配置一个目录,用于存放静态文件(如图片、CSS、JavaScript等)。
location /static {
    root /path/to/static/files;
}
  1. 配置动态请求代理:通过proxy_pass指令将动态请求转发到后端服务器的地址。
location / {
    proxy_pass http://backend_server;
}

其中,backend_server是后端服务器的地址。

  1. 配置缓存:可以通过proxy_cache指令开启缓存功能,将动态请求的响应结果缓存起来,提高性能。
proxy_cache_path /path/to/cache/dir keys_zone=my_cache:10m;
  1. 配置反向代理:如果需要将动态请求代理到多个后端服务器上,可以通过upstream指令配置一个反向代理服务器组。
upstream backend_servers {
    server backend_server1;
    server backend_server2;
}

然后在proxy_pass指令中指定反向代理服务器组的名称。

location / {
    proxy_pass http://backend_servers;
}

通过以上配置,Nginx会根据请求的URL路径来判断是静态文件请求还是动态请求,然后将请求分发到相应的后端服务器上或者从缓存中获取响应结果,实现动静分离。

git commit之后,如何撤销commit

可以使用git reset命令来撤销git commit命令的提交结果。git reset命令有三个选项,分别是--soft--mixed--hard

使用git revert HEAD^可以撤销上一次的commit

不推荐使用git reset –hard,因为这样会丢失工作区的修改

有两个表一个表很大,一个表比较小,问应该哪个表用 left join

一般情况下,如果一个表很大,而另一个表比较小,通常会将小表作为左表,大表作为右表进行 left join 操作。

使用 left join 意味着左表中的所有记录都会被包含在结果中,而右表中与左表匹配的记录也会被包含在结果中。因此,如果将小表作为左表,可以最大程度地减少需要匹配的记录数,提高查询的性能。

另外,如果小表中的记录是必需的,而大表中的记录是可选的,也可以使用 left join。这样即使在大表中没有匹配的记录,小表中的记录仍然会出现在结果中。

总结来说,如果一个表很大,而另一个表比较小,并且小表中的记录是必需的或者希望减少匹配的记录数,应该将小表作为左表,大表作为右表进行 left join。

单例模式中的懒汉模式和饿汉模式

懒汉模式:在类加载的时候不会创建实例,只有在第一次调用getInstance方法的时候才会创建实例,这种方式是线程不安全的,因为多个线程可以同时调用getInstance方法,从而创建多个实例,可以通过加锁的方式来解决线程安全的问题,但是加锁会降低效率,所以不推荐使用这种方式

饿汉模式:在类加载的时候就会创建实例,这种方式是线程安全的,但是会降低内存的使用效率,因为不管有没有使用到这个实例,都会创建实例

双重检查锁:在类加载的时候不会创建实例,只有在第一次调用getInstance方法的时候才会创建实例,这种方式是线程安全的,而且不会降低内存的使用效率,因为只有在第一次调用getInstance方法的时候才会创建实例,但是这种方式在JDK1.5之前是线程不安全的,因为JDK1.5之前的JVM对于volatile关键字的实现有问题,所以在JDK1.5之前的JVM中,需要将instance变量声明为volatile类型的,才能保证线程安全

// 懒汉模式
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
// 饿汉模式
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
// 双重检查锁
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

单例模式中实例通常使用什么关键字修饰

在单例模式中,实例通常用”private”关键字标记。这是因为单例模式要求只能有一个实例存在,所以需要将实例的构造函数设置为私有,以防止外部代码直接创建实例。同时,单例模式还需要提供一个静态方法来获取实例,这个方法通常被标记为”public”或”static”。

volatile和synchronized的区别

volatile和synchronized都可以保证多线程环境下的数据可见性和原子性,但是它们的使用场景和作用有所不同。

  1. volatile关键字

volatile关键字可以保证多线程环境下的数据可见性,即一个线程修改了共享变量的值,其他线程能够立即看到这个变化。volatile关键字可以防止编译器进行指令重排优化,保证指令的执行顺序按照代码的顺序执行。但是,volatile不能保证原子性,即不能保证多线程环境下的操作是原子性的。

  1. synchronized关键字

synchronized关键字可以保证多线程环境下的数据可见性和原子性,即一个线程在执行synchronized代码块时,其他线程不能同时执行该代码块,从而保证了操作的原子性。synchronized关键字可以保证线程的安全性,但是它的效率比较低,因为它会涉及到线程的上下文切换和锁的竞争。

因此,volatile和synchronized的区别在于:

  1. volatile关键字只能保证数据的可见性,不能保证原子性;synchronized关键字可以保证数据的可见性和原子性。

  2. volatile关键字适用于只有一个线程写入,多个线程读取的场景;synchronized关键字适用于多个线程同时读写的场景。

  3. volatile关键字的效率比synchronized关键字高,但是volatile关键字不能保证线程的安全性;synchronized关键字的效率比较低,但是可以保证线程的安全性。

指令重排

指令重排序是一种计算机处理器优化技术,它可以改变指令的执行顺序,以提高指令级并行度和执行效率。在指令重排序中,处理器可以通过重新安排指令的执行顺序来充分利用处理器的资源,例如执行单元和缓存。这样可以减少指令之间的依赖关系,提高指令的并行度,从而加快程序的执行速度。

指令重排序是通过对指令进行重新排序来实现的,但是需要保持程序的语义正确性。处理器通过使用各种技术,如乱序执行、乱序加载、乱序写入缓存等,来实现指令重排序。处理器会根据指令之间的依赖关系和数据相关性来决定是否可以进行重排序,并确保最终的执行结果与顺序执行的结果一致。

指令重排序可以提高程序的性能,但也可能引入一些问题。如果指令重排序不正确,可能会导致程序的执行结果与预期不符,从而引发各种错误。因此,处理器需要采取一些技术来保证指令重排序的正确性,如内存屏障、数据依赖检测等。

总的来说,指令重排序是一种处理器优化技术,可以提高程序的执行效率。但在使用指令重排序时,需要确保程序的语义正确性,并采取一些技术手段来保证重排序的正确性。

策略模式

策略模式是一种行为型设计模式,它可以让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。策略模式可以使算法的变化独立于使用它们的客户端(客户端即调用算法的代码),从而实现了算法的复用。

模板模式

模板模式是一种行为型设计模式,它定义了一个算法的骨架,而将一些步骤的实现延迟到子类中。模板模式可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。

Java中的Lock怎么实现的,Lock中有没有用到模板模式

在Java中,Lock是一个接口,用于实现多线程的同步。Lock接口提供了一种比synchronized关键字更灵活的方式来控制线程的访问。

Lock接口的常用实现类是ReentrantLock,它使用了模板模式来实现。模板模式是一种行为设计模式,它定义了一个操作中的算法骨架,将一些步骤的具体实现延迟到子类中。

ReentrantLock类实现了Lock接口,并提供了可重入、公平和非公平的锁定机制。它使用了模板模式来定义了锁的基本行为,包括获取锁、释放锁等操作。具体的锁定实现则由子类来完成。

ReentrantLock类中的lock()方法和unlock()方法就是模板方法,定义了获取锁和释放锁的操作流程。具体的锁定实现则由子类来完成,例如NonfairSync类和FairSync类分别实现了非公平锁和公平锁的具体实现。

通过使用模板模式,ReentrantLock类将锁的具体实现细节封装在子类中,使得锁的实现可以根据具体需求进行扩展和定制,提高了代码的可维护性和灵活性。同时,模板模式还能够提供一种标准化的锁定操作流程,简化了代码的编写。

使用join,group by会走索引吗

final finalize finally 区别

final:final关键字可以修饰类、方法和变量。修饰类时,表示该类不能被继承;修饰方法时,表示该方法不能被重写;修饰变量时,表示该变量只能被赋值一次。

finalize:finalize()方法是Object类的一个方法,用于在垃圾回收器回收对象之前调用。子类可以重写该方法,以实现资源的释放等操作。

finally:finally关键字用于定义在try代码块之后的代码块,无论是否发生异常,finally代码块中的代码都会被执行。通常在finally代码块中进行资源的释放等操作。

finally一般用于什么场景

finally一般用于进行资源的释放等操作。例如,可以在finally代码块中关闭文件、关闭数据库连接、释放锁等。

redis主从复制的原理

Redis主从复制的原理是:主节点将写命令发送给从节点,从节点接收到写命令后,将命令保存到内存中,并执行该命令,从而实现主从节点的数据同步。

Redis主从复制的过程如下:
第一次同步时分为三个阶段

  1. 建立连接,协商同步:执行replicaof命令,将从节点设置为主节点的从节点。然后从服务器会向主服务器发送一条psync命令,主节点接收到psync命令后,其中psync包含两个参数,一个是runid,一个是offset,runid是主节点的运行ID,offset是从节点的复制偏移量,主节点会根据这两个参数来判断是否可以进行全量复制,如果可以进行全量复制,则会返回一个full resync命令,否则返回一个continue命令。主服务器会用full resync命令来回复从服务器,从服务器接收到full resync命令后,会将自己的数据清空,然后执行主服务器发来的全量复制命令,全量复制是通过主节点发来的RDB文件来实现的,全量复制完成后,主节点会将自己的运行ID和复制偏移量发送给从节点,从节点接收到主节点发送的运行ID和复制偏移量后,会将这两个参数保存到自己的内存中,然后执行主节点发来的增量复制命令,增量复制是通过命令缓冲区来实现的,主节点会将写命令发送给从节点,从节点接收到写命令后,会将命令保存到自己的内存中,并执行该命令,从而实现主从节点的数据同步。

第一次同步完成后,会进入增量复制阶段,这段时间双方会维护一个TCP连接,

Redis持久化

AOF和RDB

消息队列确认机制

指的是在消息传递过程中,发送方发送消息后,接收方需要对消息进行确认,以确保消息被正确地接收和处理,RabbitMQ的消息确认机制分为两种:

生产者确认机制:生产者发送消息后,需要等待RabbitMQ服务器的确认消息,以确保消息已经被成功地发送到RabbitMQ服务器。如果RabbitMQ服务器没有收到消息或者消息发送失败,生产者会收到一个确认消息,从而可以进行重发或者其他处理。

消费者确认机制:消费者接收到消息后,需要向RabbitMQ服务器发送确认消息,以告诉服务器已经成功地接收并处理了该消息。如果消费者没有发送确认消息,RabbitMQ服务器会认为该消息没有被正确地处理,从而会将该消息重新发送给其他消费者进行处理。

大数据量存储ArrayList和LinkedList,哪个会发生OOM

当面对大数据量存储时,ArrayList更有可能发生OOM(Out of Memory)错误,而不是LinkedList。

ArrayList是基于数组实现的动态数组,它在内存中连续存储元素。当需要存储大量数据时,ArrayList需要一次性申请连续的内存空间,如果没有足够的连续空间,就会发生OOM错误。

相比之下,LinkedList是由节点组成的链表结构,每个节点包含数据和指向下一个节点的引用。当需要存储大量数据时,LinkedList只需要逐个节点地申请内存,而不需要一次性申请连续的内存空间。因此,LinkedList在面对大数据量存储时,更不容易发生OOM错误。

需要注意的是,虽然LinkedList不容易发生OOM错误,但是由于每个节点都需要额外的内存来存储引用,因此相对于ArrayList来说,LinkedList在存储大量数据时会占用更多的内存空间。

算法:给定字符串,例如“1,2,3,4”,如何单独打印出其中的数据

String str = "1,2,3,4";
String[] arr = str.split(",");
for (String s : arr) {
    System.out.println(s);
}

算法:手写最长无重复字符串

public static String getLongestSubstring(String str) {
    if (str == null || str.length() == 0) {
        return "";
    }
    int start = 0;
    int end = 0;
    int maxLen = 0;
    int maxStart = 0;
    int maxEnd = 0;
    Map<Character, Integer> map = new HashMap<>();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (map.containsKey(c)) {
            int index = map.get(c);
            if (index >= start) {
                start = index + 1;
            }
        }
        end = i;
        map.put(c, i);
        if (end - start + 1 > maxLen) {
            maxLen = end - start + 1;
            maxStart = start;
            maxEnd = end;
        }
    }
    return str.substring(maxStart, maxEnd + 1);
}
string getLongestSubstring(string str) {
    if (str.empty()) {
        return "";
    }
    int start = 0;
    int end = 0;
    int maxLen = 0;
    int maxStart = 0;
    int maxEnd = 0;
    unordered_map<char, int> map;
    for (int i = 0; i < str.length(); i++) {
        char c = str[i];
        if (map.find(c) != map.end()) {
            int index = map[c];
            if (index >= start) {
                start = index + 1;
            }
        }
        end = i;
        map[c] = i;
        if (end - start + 1 > maxLen) {
            maxLen = end - start + 1;
            maxStart = start;
            maxEnd = end;
        }
    }
    return str.substr(maxStart, maxEnd - maxStart + 1);
}

hashmap和treemap

TreeMap和HashMap都是Java中的Map接口的实现类,它们都用于存储键值对。但是它们有一些重要的区别:

  1. 内部数据结构:

    • HashMap使用哈希表来存储键值对,它通过计算键的哈希码来确定存储位置,因此查找、插入和删除操作的时间复杂度是常数级别的。
    • TreeMap使用红黑树来存储键值对,它会对键进行排序,因此可以按照键的自然顺序或者自定义顺序进行遍历,但是查找、插入和删除操作的时间复杂度是对数级别的。
  2. 排序:

    • HashMap中的键值对是无序存储的,因此遍历时的顺序是不确定的。
    • TreeMap中的键值对是有序存储的,因此可以按照键的顺序进行遍历。
  3. 性能:

    • HashMap的性能通常比TreeMap好,因为哈希表的查找、插入和删除操作的时间复杂度是常数级别的,而红黑树的时间复杂度是对数级别的。

综上所述,如果需要按照键的顺序进行遍历或者查找操作比较频繁,可以选择使用TreeMap;如果对性能要求比较高,或者不需要按照键的顺序进行遍历或者查找操作,可以选择使用HashMap。

TCP的三次握手

TCP的三次握手是指建立TCP连接时,客户端和服务器之间需要进行三次通信,以确认双方的接收能力和发送能力是否正常。三次握手的过程如下:

  1. 客户端向服务器发送SYN报文,请求建立连接。
  2. 服务器接收到SYN报文后,向客户端发送SYN-ACK报文,确认客户端的请求。
  3. 客户端接收到SYN-ACK报文后,向服务器发送ACK报文,确认服务器的请求。

为何需要三次握手?如果只进行两次握手,可能会出现以下情况:
三次握手才能实现:
阻止重复历史连接的初始化,同步双方的初始序列号,避免资源浪费
如果只有两次握手的话,那么就会出现当客户端向服务器端发送SYN报文后,宕机了,且此时由于网络阻塞问题导致这次的SYN报文滞留在网络中,一段时间后,客户端重启再次向服务器端发送一个SYN报文,而上一次发送的syn报文此时到达了服务器端,服务器先处理这个syn报文回复给服务器,但是当服务器将这个syn-ack报文发送给客户端后,客户端对比其中的seq num发现不是最新的那个报文ack消息,拒绝了这次连接,但是由于没有第三次握手,导致服务器并不知道这个连接被拒绝了,由于此时的服务器端已经处在连接等待状态,所以会一直等待客户端的ack报文,导致服务器端的资源浪费。
其次两次握手无法同步双方初始化序列号,只能保证一方的序列号能够被对方成功接受了,但是无法保证双方的序列号是一致的,这样就会导致双方的序列号不一致,从而导致后续的数据传输出现问题。

volatile关键字

volation作用是保证变量的可见性,即一个线程修改了变量的值,其他线程能够立即看到这个变化。volatile关键字可以防止编译器进行指令重排优化,保证指令的执行顺序按照代码的顺序执行。
但是由于Java运算并非原子操作,所以volatile不能保证线程的安全性,即不能保证多线程环境下的操作是原子性的。
在工作内存中,每次使用volatile修饰的变量前都必须先从主内存刷新最新的值,这保证了当前线程能看见其他线程对volatile修饰的变量所做的修改后的值。
在工作内存中,每次修改volatile修饰的变量后都必须立刻同步回主内存中,这保证了其他线程可以看到自己对volatile修饰的变量所做的修改。

volatile实现禁止指令重排是通过内存屏障来实现的,内存屏障是一种CPU指令,它可以保证指令重排时的正确性。

volatile的使用限制:

  1. 运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束

使用场景

  1. 状态标记量:例如常见的秒杀活动中可以用volatile来标记是否还有库存
  2. 双重检测机制实现单例模式:例如常见的单例模式中可以用volatile来标记实例变量,从而实现线程安全的单例模式

synchronized关键字原理

synchronized关键字可以保证多线程环境下的数据可见性和原子性,即一个线程在执行synchronized代码块时,其他线程不能同时执行该代码块,从而保证了操作的原子性。

synchronized关键字的原理是:依赖于对象的对象头中的monitor来实现锁的功能,monitor是依赖于底层操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么使用synchronized会导致性能降低的原因。

手写双重单例检验

public class Singleton{
    private Singleton() {}
    private static volatile Singleton instance=null;
    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
                if(instance==null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}

线程池的核心参数以及拒绝策略,怎么用的

线程池的核心参数包括:核心线程数、最大线程数、线程存活时间、任务队列、拒绝策略。
拒绝策略包括:AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。

AbortPolicy:直接抛出异常,阻止系统正常工作。适用于对任务丢失敏感的场景,当线程池无法接受新任务时,希望立即直到并处理该异常,从而避免任务丢失。例如在调用者线程中捕获该异常并进行相应处理。

CallerRunsPolicy:有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。不会抛出异常。使用场景:适用于希望调用者自己处理被拒绝的任务的场景,通常是由调用者自身的线程来执行被拒绝的任务。例如在调用者线程中打印日志。

DiscardOldestPolicy:会丢弃任务队列中的头结点,通常是存活时间最长并且未被处理的任务。使用场景:适用于对新任务优先级较高的场景,当线程池无法接受新任务时,会丢弃一些等待时间较长的旧任务,以便接受新任务。

DiscardPolicy:直接丢弃任务,不会抛出异常。使用场景:适用于对任务丢失不敏感的场景,当线程池无法接受新任务时,会直接丢弃新任务,从而避免任务堆积。

jvm内存模型,每块干什么的

JVM内存模型主要有程序计数器,虚拟机栈,本地方法栈,堆,方法区,直接内存构成。

程序计数器:用于记录当前线程执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

设计模式的了解,讲讲工厂模式

8:jvm垃圾回收过程
9:创建对象的几种方式

手写最长无重复字符串

import java.util.HashSet;
import java.util.Set;
public class LongestSubstrWithoutRepeating{
    public static longestSubstring(String s){
        int left=0,right=0;
        int maxLength=0;
        String res="";
        Set<Character> set=new HashSet<>();
        while(right<s.length()){
            if(!set.contains(s.charAt(right))){
                set.add(s.charAt(right));
                right++;
                if(right-left>maxlength){
                    maxLength=right-left;
                    res=s.substring(left,right);
                }
        }else{
            set.remove(s.charAt(left));
            left++;
            }
        }
    return res;
    }
    public static main(String[] args){
        String s= "abcdabc";
        System.out.println(longestSubstring(s));
    }
}

手撕:链表反转(要自己跑通示例)
简单了解下项目背景
redis基本数据类型
常见缓存问题及解决方法
http,tcp,ip各解决什么问题
遇到最大的困难
(晚上就约了二面)

二面70min:
逐个问项目细节+穿插八股
文件传输是用tcp还是udp好,分析原因
http和websocket区别
redis基本数据类型
介绍一下reactor模式,事件驱动方式
深度学习相关
手撕:链表两个节点反转
Java GC的流程
ThreadLocal 可以保证线程安全吗?内存泄漏
http 和 https 的区别
三次握手四次挥手
Java 创建对象的方式有哪些
arraylost 扩容流程
join 和 yield 区别

本文作者:GWB
当前时间:2023-11-09 11:11:09
版权声明:本文由gwb原创,本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议。
转载请注明出处!