Java后端综合面试题
如果不用平衡二叉树,一直插入节点会怎样
如果不使用平衡二叉树,而是一直向二叉搜索树中插入节点,可能会导致树的高度不断增加,从而使得树的性能下降。如果树的高度过高,查找、插入和删除操作的时间复杂度可能会变得非常高,甚至退化为O(n)的复杂度,其中n是树中节点的数量。
如果树的高度过高,还可能会导致树的不平衡,使得某些子树比其他子树深得多。这将使得在深度较大的子树中进行查找、插入和删除操作的时间复杂度更高,从而影响整个树的性能。
因此,在不使用平衡二叉树的情况下,为了维持树的性能,我们需要确保插入节点的顺序尽可能随机,以尽可能地平衡树的结构。此外,还可以考虑使用其他类型的树结构,例如B树或哈希表等,以更好地支持插入操作。
请详细解释三次握手四次挥手过程
TCP协议中,建立连接需要进行三次握手,断开连接需要进行四次挥手。具体的报文如下:
- 三次握手(Three-way Handshake)
a. 客户端向服务器发送一个SYN报文,其中SYN标志位被设置为1,表示客户端请求建立连接。
iniCopy code客户端发送的SYN报文: Sequence Number=x, SYN=1, ACK=0
b. 服务器收到SYN报文后,向客户端发送一个SYN/ACK报文,其中SYN和ACK标志位都被设置为1,表示服务器同意建立连接,并告诉客户端它的初始序列号。
iniCopy code服务器发送的SYN/ACK报文: Sequence Number=y, SYN=1, ACK=1, Acknowledgement Number=x+1
c. 客户端收到SYN/ACK报文后,向服务器发送一个ACK报文,其中ACK标志位被设置为1,表示客户端确认收到了服务器的响应,并告诉服务器它的初始序列号。
iniCopy code客户端发送的ACK报文: Sequence Number=x+1, ACK=1, Acknowledgement Number=y+1
至此,三次握手完成,连接建立成功。
- 四次挥手(Four-way Handshake)
a. 客户端向服务器发送一个FIN报文,其中FIN标志位被设置为1,表示客户端要关闭连接。
iniCopy code客户端发送的FIN报文: Sequence Number=u, FIN=1, ACK=1, Acknowledgement Number=v
b. 服务器收到FIN报文后,向客户端发送一个ACK报文,其中ACK标志位被设置为1,表示服务器收到了客户端的关闭请求。
iniCopy code服务器发送的ACK报文: Sequence Number=v, ACK=1, Acknowledgement Number=u+1
c. 服务器向客户端发送一个FIN报文,其中FIN标志位被设置为1,表示服务器也要关闭连接。
iniCopy code服务器发送的FIN报文: Sequence Number=w, FIN=1, ACK=1, Acknowledgement Number=u+1
d. 客户端收到FIN报文后,向服务器发送一个ACK报文,其中ACK标志位被设置为1,表示客户端确认收到了服务器的关闭请求。
iniCopy code客户端发送的ACK报文: Sequence Number=u+1, ACK=1, Acknowledgement Number=w+1
至此,四次挥手完成,连接断开成功。
服务端需发送数万条数据给客户端,如何优化
- 分批次发送数据:将数万条数据分成多个批次发送,每次发送一部分数据,客户端接收完毕后再请求下一批数据。这种方式可以避免一次性发送大量数据导致网络拥堵和资源占用过多的问题。
- 使用数据压缩算法:对数据进行压缩,减小数据的大小,从而减少传输的时间和网络负载。常见的数据压缩算法有Gzip和Deflate等。
- 使用分块传输编码(Chunked Transfer Encoding):将大文件或大数据分成多个块进行传输,每个块的大小可以根据网络情况动态调整。这种方式可以避免一次性传输大文件或大数据导致的网络拥堵和传输失败的问题。
- 使用缓存机制:在服务端缓存数据,客户端每次只请求需要的部分数据,避免一次性请求全部数据导致的网络拥堵和资源占用过多的问题。
- 使用HTTP/2协议:HTTP/2协议支持多路复用和头部压缩等特性,可以提高传输效率,减少网络负载。
如果要回表,数量很多要做分页查询,如何效率好一些(limit优化?数据不连续的情况下呢)
对于需要进行分页查询的情况,可以考虑使用索引来优化查询效率。如果数据量很大,可以使用分页查询来减少一次性查询的数据量,从而提高查询效率。
在使用分页查询时,可以使用 LIMIT 和 OFFSET 关键字来指定查询的起始位置和查询的数量。但是,当数据量很大时,使用 LIMIT 和 OFFSET 可能会导致查询效率变慢,因为它们需要扫描整个表来找到指定的行。
为了避免这种情况,可以使用“游标分页”技术。这种技术使用一个游标来记录查询的位置,然后在下一次查询时使用这个游标来查询下一页的数据。这样可以避免扫描整个表,从而提高查询效率。
如果数据不是连续的,也可以使用索引来优化查询效率。可以根据数据的分布情况创建合适的索引,以便快速查找需要的数据。
总之,为了提高分页查询的效率,可以考虑使用索引和游标分页技术。
讲一下Bean的生命周期
- 实例化:当程序创建Bean对象时,通过构造函数或者工厂方法等方式来创建Bean实例。
- 属性赋值:在Bean实例化后,程序将会为Bean的各个属性赋值。属性可以通过直接赋值、通过构造函数、通过依赖注入等方式进行赋值。
- BeanPostProcessor的前置处理器:在属性赋值完成后,容器会调用注册的BeanPostProcessor前置处理器进行处理。前置处理器可以对Bean的属性进行修改或增强。
- 初始化方法调用:在前置处理器执行完成后,容器会调用Bean的初始化方法进行初始化,可以通过实现InitializingBean接口或者在XML配置文件中配置init-method来指定初始化方法。
- BeanPostProcessor的后置处理器:在Bean的初始化方法调用完成后,容器会调用注册的BeanPostProcessor后置处理器进行处理。后置处理器可以对Bean的属性进行修改或增强。
- 使用:在Bean的初始化完成后,程序可以使用该Bean进行相应的业务操作。
- 销毁:当容器关闭时,容器会调用Bean的销毁方法进行清理操作,可以通过实现DisposableBean接口或者在XML配置文件中配置destroy-method来指定销毁方法。
实际开发如何避免死锁
1.避免持有多个锁:持有多个锁是死锁发生的主要原因之一,因此在设计多线程程序时应尽量避免同时持有多个锁。
2.按照固定的顺序获取锁:如果不同的线程需要获取多个锁,可以按照固定的顺序获取锁,从而避免死锁的发生。
3.设置超时时间:在获取锁的过程中,可以设置超时时间,如果在规定时间内无法获取锁,就放弃获取并释放已经持有的锁,从而避免死锁的发生。
4.使用非阻塞算法:非阻塞算法不会让线程陷入等待状态,从而避免死锁的发生。
5.尽量减少锁的持有时间:锁的持有时间越长,死锁的发生概率就越高,因此在设计多线程程序时应尽量减少锁的持有时间。
6.使用死锁检测工具:在开发过程中,可以使用死锁检测工具来检测和解决潜在的死锁问题。
例如,假设有两个线程 A 和 B,它们需要获取两个锁 X 和 Y,如果 A 先获取了锁 X,然后 B 获取了锁 Y,接着 A 尝试获取锁 Y,而 B 尝试获取锁 X,就会发生死锁。为了避免这种情况,可以让 A 和 B 按照固定的顺序获取锁,比如先获取锁 X 再获取锁 Y,从而避免死锁的发生。
Redis宕机后怎么恢复
1.使用 AOF 持久化文件进行恢复:如果 Redis 配置了 AOF 持久化,可以通过 AOF 文件来进行数据恢复。在 Redis 重新启动时,Redis 会自动读取 AOF 文件中的操作记录,执行恢复操作。需要注意的是,如果 AOF 文件过大,恢复时间可能会比较长。
2.使用 RDB 持久化文件进行恢复:如果 Redis 配置了 RDB 持久化,可以通过 RDB 文件来进行数据恢复。在 Redis 重新启动时,Redis 会自动读取 RDB 文件中的数据,执行恢复操作。需要注意的是,如果 RDB 文件过大,恢复时间可能会比较长。
3.使用 Redis Sentinel 进行自动故障转移:如果 Redis 配置了 Redis Sentinel,可以通过自动故障转移来恢复数据。在 Redis Sentinel 检测到 Redis 宕机后,会自动将主节点切换到备用节点上,从而实现数据恢复。需要注意的是,Redis Sentinel 需要在 Redis 集群中配置多个节点,才能实现自动故障转移。
4.使用 Redis Cluster 进行自动故障转移:如果 Redis 配置了 Redis Cluster,可以通过自动故障转移来恢复数据。在 Redis Cluster 检测到节点宕机后,会自动将槽位迁移至其他节点,从而实现数据恢复。需要注意的是,Redis Cluster 需要在 Redis 集群中配置多个节点,才能实现自动故障转移。
为什么要使用线程池?线程池有什么优点?
- 提高性能:线程池可以重复利用已经创建的线程,避免了线程的频繁创建和销毁,从而提高了程序的性能。
- 提高响应速度:线程池可以减少线程的创建和销毁时间,从而提高了程序的响应速度,特别是在高并发的情况下,可以避免线程创建和销毁的时间开销。
- 节约资源:线程池可以限制线程的数量,避免线程数量过多导致系统资源的浪费,从而节约了系统资源。
- 提高程序的可管理性:线程池可以统一管理线程的创建、销毁、调度等操作,从而提高了程序的可管理性,减少了程序的复杂度。
- 避免死锁:线程池可以限制线程的数量,避免线程数量过多导致死锁等多线程并发问题的发生。
如何在1亿个数中找出最大的100个数(top K问题)
使用堆排序(Heap Sort)来解决。堆排序是一种基于二叉堆数据结构的选择排序算法,可以在O(n log k)的时间复杂度内找出最大的k个数。
具体实现步骤如下:
- 建立一个大小为k的小根堆(Min Heap)。
- 遍历这1亿个数,对于每个数,如果小根堆未满,就将其加入堆中;如果小根堆已满,就将该数与堆顶元素比较,如果该数比堆顶元素大,就将堆顶元素替换为该数,并对堆进行调整,保证堆仍然是小根堆。
- 遍历完所有数后,堆中保存的就是最大的k个数。
下面是Python的实现代码:
def top_k(nums, k): heap = [] for num in nums: if len(heap) < k: heapq.heappush(heap, num) else: if num > heap[0]: heapq.heapreplace(heap, num) return heap # 测试代码 nums = [3, 2, 1, 5, 6, 4] print(top_k(nums, 2))
输出结果为:[5, 6],符合预期。
return { user, attachImageUrl: HttpManager.attachImageUrl, ...toRefs(state), ...methods } attachImageUrl: (url) => `${getBaseURL()}/${url}
为什么是三次握手,不是两次,也不是四次
为什么不是两次握手呢?如果只有两次握手,那么客户端发送SYN包后,服务器收到后就会发送ACK包,这时候连接就建立了。但是,如果ACK包在传输过程中丢失了,那么客户端就会一直等待,而服务器则不知道客户端是否收到了ACK包,也不知道客户端是否还想建立连接。因此,三次握手可以确保双方都能够收到对方的消息,从而建立一个可靠的连接。
为什么不是四次握手呢?如果使用四次握手,那么客户端发送SYN包后,服务器收到后就会发送ACK包和SYN包,连接建立后,客户端再发送一个ACK包,表示确认服务器的请求。这种方式会增加一个RTT(Round Trip Time)的延迟,降低连接的建立速度。因此,三次握手是一种更为优秀的方式,可以在保证可靠性的同时,尽可能地减少连接建立的时间。
如何保证数据库和Redis的一致性
- 双写:双写是一种常用的方法,即每次更新数据库时,同时也更新Redis缓存。这样可以保证Redis和数据库中的数据一致,但是会增加系统的复杂度和延迟。
- 异步更新:异步更新是一种常用的方法,即每次更新数据库时,异步地更新Redis缓存。这样可以减少系统的复杂度和延迟,但是会增加数据不一致的风险。
- 加锁:加锁是一种保证数据一致性的方法,即在更新Redis缓存和数据库时,先获取一个锁,确保只有一个线程可以进行更新操作。这样可以保证数据的一致性,但是会增加系统的复杂度和延迟。
什么是布隆过滤器?他有什么应用场景
布隆过滤器(Bloom Filter)是一种空间效率高、误判率低的数据结构,用于判断一个元素是否在一个集合中。它由一个位数组和多个哈希函数组成,可以用于快速判断一个元素是否属于一个集合,同时可以节省大量的存储空间。
具体来说,布隆过滤器的基本原理是将每个元素通过多个哈希函数映射到位数组中的多个位置,并将这些位置的值设置为1。当判断一个元素是否在集合中时,将该元素通过相同的哈希函数映射到位数组中的多个位置,并检查这些位置的值是否都为1。如果有任何一个位置的值为0,则可以确定该元素不在集合中;如果所有位置的值都为1,则不能确定该元素是否在集合中,可能会出现误判。
布隆过滤器的应用场景包括:
缓存穿透、爬虫去重、黑名单过滤。
concurrentHashmap的锁和synchronized的锁有什么区别
ConcurrentHashMap
和 synchronized
是 Java 中用于实现线程安全的两种不同方式。它们在锁的粒度、并发性和适用场景等方面有一些区别。
锁的粒度:
ConcurrentHashMap
: 它是基于分段锁的并发哈希表。内部将数据分成多个段(Segment),每个段拥有自己的锁。这样,在多线程环境下,不同的线程可以同时访问不同的段,从而提高并发性。只有在涉及同一个段时才会出现竞争。synchronized
: 是使用在方法或代码块级别的锁,当一个线程进入 synchronized 代码块时,其他线程必须等待锁的释放。这种锁的粒度相对较大,可能会导致其他线程长时间等待,影响并发性。
并发性能:
ConcurrentHashMap
: 在高并发场景下,ConcurrentHashMap
可以更好地扩展,因为它将数据分成多个段,减少了竞争点。只有在修改同一个段的数据时才可能会出现竞争。这使得它在高并发访问时表现较好。synchronized
: 使用synchronized
的代码块或方法在高并发环境下可能会出现较多的竞争,因为每个线程都必须按顺序获得锁。这可能导致性能瓶颈,尤其是在访问共享资源频繁的情况下。
适用场景:
ConcurrentHashMap
: 适用于需要高并发性的场景,特别是读多写少的情况。由于它的分段锁机制,读操作可以并发进行,写操作只涉及到单个段的锁,所以适用于大量读写操作同时进行的情况。synchronized
: 适用于简单的线程安全问题,或者对于特定的代码块需要原子性操作的情况。虽然在 Java 5 之后,Java 提供了更多并发工具,但是synchronized
仍然是保证线程安全的有效方式之一。
总结:ConcurrentHashMap
在高并发读写场景中具有较好的性能,适合读多写少的情况。而 synchronized
适合简单的线程安全问题和代码块级别的同步需求。在选择使用时,需要根据实际的并发需求来决定哪种锁机制更适合。
怎么打破双亲委派机制
Java 的类加载机制中存在双亲委派模型,它是一种安全机制,用于防止类的重复加载和保证类加载的一致性。虽然它在大多数情况下是有益的,但有时我们可能需要打破这种机制,例如在某些特定场景下需要自定义类加载器加载特定的类。
要打破双亲委派机制,可以通过自定义类加载器来实现。自定义类加载器可以继承自 java.lang.ClassLoader
类,然后重写 loadClass()
方法,以自己的加载逻辑来加载类。
什么是分布式锁,如何使用redis建立分布式锁
分布式锁是在分布式系统中,用于协调多个节点或线程之间对共享资源的访问的一种机制。它的目的是确保在某个时刻只有一个节点(或线程)能够访问共享资源,从而避免数据不一致或并发冲突的问题。
Redis 是一个常用的内存数据库,也是一个很好的工具,可以用于实现分布式锁。在 Redis 中,可以通过以下两种方式来实现基于 Redis 的分布式锁:
- 使用 SETNX 命令:SETNX 命令用于设置键的值,但只在键不存在时起作用。这一特性可以用于实现分布式锁。假设我们要实现一个名为 "lock_key" 的分布式锁,可以使用以下逻辑来尝试获取锁:需要注意的是,为了防止死锁等问题,应该在获取锁后,为锁设置一个过期时间,确保即使在某些情况下锁未被显式释放,也能自动释放锁。
- 使用 RedLock 算法:RedLock 是 Redis 官方提供的一种分布式锁算法,它是一种更复杂但更可靠的分布式锁实现方式。RedLock 使用多个 Redis 节点来确保锁的高可用性和鲁棒性。它的原理是在多个独立的 Redis 节点上创建相同的锁,并设置相同的过期时间。在释放锁时,需要在大部分(比如大多数节点的数量)的 Redis 节点上执行删除操作,才视为成功释放锁。使用 RedLock 需要确保 Redis 节点的数量足够,同时网络延迟等因素也需要考虑,以确保 RedLock 的正确性。
无论使用哪种方式实现分布式锁,都需要考虑各种异常情况和并发问题。在 Redis 中实现分布式锁时,需要特别注意过期时间、防止误删除、处理锁超时等问题,以确保锁的正确使用和高可用性。
重载和重写的区别
重载1.重载Overload是一个类中多态性的一种表现2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准
重写(Override)
从字面上看,重写就是 重新写一遍的意思。其实就是在子类中把父类本身有的方法重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意子类函数的访问修饰权限不能少于父类的。
redis 和 memcached 什么区别?为什么高并发下有时单线程的 redis 比多线程的 memcached 效率要高? 区别:
1.memcached 可缓存图片和视频。redis 支持除 k/v 更多的数据结构;
2.redis 可以使用虚拟内存,redis 可持久化和 aof 灾难恢复,redis 通过主从支持数据备份;
3.redis 可以做消息队列。 原因:memcached 多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
MySQL主存复制读写分离后,主存之间存在读写延迟,在操作内进行后读,未读到正确数据怎么办?
分库分表后如何读写分离?
手写一个工厂模式
public class SendFactory { public Sender produce(String type) { if (type.equals("mail")) return new MailSender; else if (type.equals("sms")) return new SmsSender; else return new DefaultSender; } }
手写一个单例模式
public class Singleton { private Singleton instance = null; public static Singleton getSingleton() { if (instance == null) { instance = new Singleton(); } return instance; } }
手写一个promise
class Promise { constructer(executor) { this.status = 'PENDING' this.value = undefined this.reason = undefined let resolve = (value) => { if (this.status == 'PENDING') { this.status = 'FULFILLED' this.value = value } } let reject = (reason) => { if (this.status == 'PENDING') { this.status = 'RECJECTED' this.reason = reason } } try { executor(resolve, reject) } catch(err) { reject(err) } } then(onFulfilled, onRejected) { if (this.status == 'PENDING') { onFulfilled(this.value) } if (this.status == 'RECJECTED') { onRejected(this.reason) } } }