(高频问题)141-160 计算机 Java后端 实习 and 秋招 面试高频问题汇总

专栏简介

141. Redis 与其他实现幂等性的策略

幂等性(Idempotency)是确保一个操作无论执行多少次,最终产生的效果都与执行一次相同的重要系统特性。它在分布式系统、网络通信(尤其是 RESTful API 设计)中至关重要,特别是在处理可能重复的请求时,能够有效保障数据一致性和系统稳定性

幂等性在多个场景下都有应用。例如,在 HTTP/1.1 规范中,GET、PUT、DELETE 等方法被设计为幂等的。这意味着重复执行同一个 DELETE 请求,其效果与执行一次相同,即目标资源被删除。另一个典型场景是支付系统,一次付款操作无论被触发多少次,用户的账户实际扣款金额应当只有一次,防止因重复请求导致多次扣款。

使用 Redis 实现幂等性是一种常见且高效的方法。Redis 作为一个高性能的键值存储系统,其特性非常适合实现幂等控制:

可以利用 Redis 来实现幂等性检查。核心思路是为每一个需要保证幂等性的操作生成一个唯一的请求标识符(例如 Token 或基于业务参数生成的唯一键)。在执行实际业务操作之前,使用 Redis 的 SETNX (Set if Not Exists) 命令尝试将这个唯一标识符作为键写入 Redis

  • 如果 SETNX 命令返回 1,表示该键是首次写入,说明这是对此操作的第一次请求(或之前的请求未成功处理),此时可以继续执行业务逻辑。
  • 如果 SETNX 命令返回 0,表示该键已存在,说明相同的操作请求已经被处理或正在处理中,此时应拒绝执行当前请求,直接返回之前的处理结果或提示重复操作。

为了防止标识符永久占用 Redis 内存,通常会为这个键设置一个合理的过期时间。这样,即使操作完成后忘记显式删除,标识符也会在一段时间后自动清理。

除了 Redis,还有多种其他方式可以实现幂等性,具体选择哪种方式取决于应用的具体场景和需求:

  • 数据库唯一约束:这是非常常用且可靠的方法。通过在数据库表中为关键业务字段(如订单号、交易流水号)设置唯一索引或唯一约束。当尝试插入重复记录时,数据库层面会直接报错,应用程序捕获这个错误即可识别出重复操作,从而保证了插入操作的幂等性。
  • 乐观锁机制:主要用于更新操作的幂等性保证。通过在数据表中增加一个版本号(version)或时间戳字段。每次更新数据前,先读取当前版本号,然后在提交更新时,检查数据库中的版本号是否与读取时一致。只有一致时才执行更新,并将版本号加一。若不一致,则表示数据已被其他操作修改,当前更新操作失败,可选择重试或报错。
  • 状态机机制:对于涉及多个状态流转的复杂业务流程,可以设计一个严谨的状态机模型。每个操作都必须依据当前状态和输入,才能转移到明确的下一个状态。后续的重复请求如果发现当前状态不符合前置条件(例如订单已支付状态下再次收到支付请求),则不执行状态变更,从而保证幂等性。
  • 令牌机制(Token):在客户端发起请求前,先向服务端申请一个一次性的唯一令牌(Token)。客户端在提交业务请求时携带此令牌。服务端接收到请求后,首先校验令牌的有效性并记录其已被使用(例如存入 Redis 或数据库并设置状态)。只有未被使用过的有效令牌对应的请求才会被处理。后续携带相同令牌的请求将被视为重复请求而被拒绝。

142. Linux 中删除正在写入文件的行为与影响

在 Linux 系统中,当使用 rm 命令尝试删除一个正在被某个进程写入的文件时,会发生一系列特定的行为:

首先,rm 命令会从文件系统中移除该文件的目录项(directory entry)。这意味着文件名与 inode 的链接被断开,因此通过原来的路径或在文件所在的目录中,该文件将不再可见。

然而,如果有进程在此之前已经打开了该文件并持有其文件描述符(file descriptor),那么这个进程并不会受到 rm 命令的影响。Linux/UNIX 文件系统的设计允许这种情况发生,因为进程操作文件是基于文件描述符,而非文件名。只要文件描述符保持打开状态,进程就可以继续通过该描述符对文件进行读写操作

文件的实际磁盘空间并不会在执行 rm 命令后立即释放。文件所占用的磁盘空间,只有在最后一个持有该文件打开状态的文件描述符被关闭(无论是进程正常关闭文件还是进程终止),并且文件的所有硬链接(如果有的话)都被删除之后,才会被操作系统回收。在此之前,即使文件名消失,数据仍然存在于磁盘上,并可被持有文件描述符的进程访问。

总结来说,删除一个正在被写入的文件,会导致文件名消失,但写入进程可以继续写入数据。文件占用的存储空间直到所有引用(打开的文件描述符、硬链接)都消失后才会被释放。这种行为有一个重要的应用场景:创建临时文件时,可以先创建文件,获取其文件描述符,然后立即使用 rm 删除文件名。这样,即使程序异常退出,也不会在文件系统中留下垃圾文件,因为文件空间会在进程结束时自动回收。

143. 操作系统中的进程状态及其转换

在操作系统中,一个进程在其生命周期内会经历不同的状态,这些状态反映了进程当前的活动情况以及与系统资源的交互。虽然具体状态的名称和数量可能因操作系统而异,但通常包含以下几种核心状态:

  • 新建(New):进程刚刚被创建,正在进行初始化,操作系统已为其分配了必要的资源,但尚未准备好执行。
  • 就绪(Ready):进程已经具备执行所需的所有条件(除 CPU 外),已被加载到内存中,并放入就绪队列,等待操作系统调度程序分配 CPU 时间片。
  • 运行(Running):进程的指令正在 CPU 上执行。此时,进程获得了 CPU 的控制权。
  • 等待(Waiting)或阻塞(Blocked):进程因为需要等待某个事件的发生而暂时停止执行,例如等待 I/O 操作完成、等待获取某个锁或资源、等待信号等。即使 CPU 空闲,处于等待状态的进程也不会被调度执行。
  • 终止(Terminated)或退出(Exited):进程已经完成了执行,或者因为错误、被用户或操作系统强制结束。此时进程不再执行,但其相关信息可能仍保留一段时间,以便操作系统进行资源回收或父进程查询退出状态。

除了上述基本状态,还存在一些特殊状态,用于更精细地管理进程,尤其是在内存资源紧张时:

  • 挂起就绪(Suspended Ready):进程具备运行条件(处于就绪态),但由于内存不足等原因,其内存映像被暂时换出到外存(如硬盘)。虽然它在逻辑上是就绪的,但物理上不在内存中,因此无法立即被调度运行。当内存可用时,操作系统需要将其重新调入内存,然后才能转换为真正的就绪状态。
  • 挂起阻塞(Suspended Blocked):进程原本处于等待(阻塞)状态,同时其内存映像也被换出到外存。它既在等待某个事件发生,又不在内存中。只有当等待的事件发生后,它会先转变为挂起就绪状态,待被调入内存后,才能变为就绪状态,最终才有机会运行。

进程状态之间会根据特定事件发生转换:

一个新创建的进程完成初始化后进入就绪状态。当调度器选择一个就绪进程时,它变为运行状态。正在运行的进程可能因为分配的时间片用完而回到就绪状态;或者因为请求 I/O 或等待资源而进入等待状态;或者正常完成任务进入终止状态。当等待的事件发生(如 I/O 完成)时,等待状态的进程会转变为就绪状态,重新等待 CPU 调度。

144. ThreadLocal 的底层实现原理

ThreadLocal 是 Java 提供的一种机制,用于实现线程局部变量,即每个线程都能访问到该变量的一个独立副本,从而避免了多线程间共享状态带来的并发问题。

其底层实现的核心在于每个 Thread 对象内部维护的一个 ThreadLocalMap 类型的成员变量 threadLocals。ThreadLocal.ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它类似于一个定制化的哈希映射表。这个 Map 的键是 ThreadLocal 对象本身,而值则是该线程为这个 ThreadLocal 变量存储的实际副本。

当调用 ThreadLocal 对象的 set(T value) 方法时,它首先获取当前线程(Thread.currentThread()),然后访问该线程的 threadLocals 映射表。如果这个映射表存在,就以当前的 ThreadLocal 对象为键,将传入的 value 存储或更新到表中。如果映射表不存在,会先创建一个新的 ThreadLocalMap 并关联到当前线程,然后再进行存储。

当调用 get() 方法时,同样是获取当前线程的 threadLocals 映射表,并以当前的 ThreadLocal 对象为键查找对应的值。如果找到了,就返回该值。如果映射表不存在或者表中没有对应的条目,get() 方法会调用 initialValue() 方法(如果子类重写了的话)来获取一个初始值,并将这个初始值存入映射表后再返回。

一个关键的设计点是,ThreadLocalMap 中的键(即 ThreadLocal 对象)是以弱引用(WeakReference)的形式存储的。这意味着,如果一个 ThreadLocal 对象在外部不再有强引用指向它(例如,持有 ThreadLocal 实例的类对象被回收),那么即使在某个线程的 ThreadLocalMap 中还存在这个 ThreadLocal 对象作为键,它也可能被垃圾收集器回收。这样设计主要是为了防止内存泄漏。当键被回收后,ThreadLocalMap 在后续操作(如 set, get, remove)时会尝试清理这些键对应的无效条目(值仍然是强引用,需要显式清理或等待线程结束)。

尽管有弱引用机制,但在线程池等线程长时间复用的场景下,如果 ThreadLocal 变量使用完毕后不主动清理,其对应的值仍然会保留在线程的 ThreadLocalMap 中,可能导致内存泄漏。因此,最佳实践是在使用完 ThreadLocal 变量后,显式调用 remove() 方法来移除当前线程对应的值,这有助于及时释放内存并减少潜在的内存泄漏风险

145. 设计模式:桥接模式及其应用

桥接模式(Bridge Pattern)是一种结构型设计模式,其核心思想是将抽象部分与它的实现部分分离,使它们都可以独立地变化。这种模式主要用于解决当一个类存在多个独立变化的维度时,因使用继承导致类爆炸性增长的问题。

考虑一个例子:我们有一个 Shape(形状)抽象类,可以派生出 Circle(圆形)和 Square(方形)等具体形状。现在,我们希望为这些形状添加颜色属性,比如 Red(红色)和 Blue(蓝色)。如果直接使用继承,就需要为每种形状和颜色的组合创建一个类,如 RedCircle, BlueCircle, RedSquare, BlueSquare。随着形状和颜色维度的增加(例如增加 Triangle 形状或 Green 颜色),需要创建的类数量会呈指数级增长(形状数量 * 颜色数量),导致类体系非常臃肿和难以维护。问题的根源在于我们试图在两个独立的维度——形状与颜色上同时使用继承进行扩展。

桥接模式通过将继承关系转变为组合关系来解决这个问题。它建议抽取其中一个变化维度(例如颜色)形成一个独立的类层次结构(实现部分),然后在另一个维度(例如形状,作为抽象部分)的类中包含一个指向这个独立层次结构接口的引用

具体到上述例子,我们可以创建一个 Color 接口(实现部分的接口),并有 RedColor, BlueColor 等具体实现类。然后,在 Shape 抽象类中,添加一个 Color 类型的成员变量。Shape 类可以将所有与颜色相关的操作(如绘制颜色)委托给其持有的 Color 对象来完成这个从 Shape 指向 Color 的引用就构成了两者之间的“桥梁”

通过这种方式,Shape 的层次结构(抽象部分)和 Color 的层次结构(实现部分)可以独立地进行扩展。如果需要增加一个新的形状(如 Triangle),只需创建一个继承自 Shape 的新类即可,无需关心颜色。同样,如果需要增加一种新的颜色(如 GreenColor),只需创建一个实现 Color 接口的新类即可,无需修改任何 Shape 相关的类。客户端代码可以根据需要组合不同的形状和颜色对象。

因此,桥接模式很好地体现了“开闭原则”,允许系统在不修改现有代码的情况下进行扩展。它通过组合/聚合关联将原本耦合在一起的多个变化维度分离,让它们能够独立演化,从而降低了系统的复杂度和耦合度。再理解“将抽象部分与它的实现部分分离”这句话,就是指将系统中可能沿着多个维度变化的特性识别出来,把这些多角度的变化分离成独立的继承体系,再通过组合将它们连接起来,让它们可以独立变化,减少彼此间的耦合

146. 常见的限流算法及其原理分析

限流是系统保护的关键措施,用于控制访问速率,防止因瞬时或持续的高并发请求导致服务过载。以下是几种核心的限流算法:

计数器算法(Fixed Window Counter) 是最基础的限流方式。它在预设的时间窗口内(例如,每秒)维护一个请求计数器。每当请求到达时,计数器加一。如果计数器的值超过了设定的阈值,则拒绝新的请求。此算法实现简单,但存在显著的临界问题:在时间窗口切换的边缘,可能出现两倍于阈值的流量集中通过(例如,第一秒的最后时刻和第二秒的最开始时刻的流量叠加),导致瞬时流量超限。

漏桶算法(Leaky Bucket) 将请求处理过程形象化为一个底部有孔洞的桶。请求如同水流注入桶中,而桶以恒定的速率通过孔洞漏出(处理请求)。无论进入的水流速率多快,流出的速率始终不变。如果水流注入过快导致桶内水量超出容量,多余的水(请求)就会溢出(被拒绝)。漏桶算法能够有效地平滑输出速率,强制请求以一个稳定的速率被处理,但缺点在于无法有效利用系统在流量低谷时的处理能力,对于允许一定程度突发流量的场景适应性较差,因为其处理速率是固定的。

令牌桶算法(Token Bucket) 是对漏桶算法的一种改进,旨在兼顾速率限制和一定的突发流量处理能力。系统以恒定的速率向桶中生成令牌。每个到达的请求需要消耗一个(或多个)令牌才能被处理。如果桶中有足够的令牌,请求则被立即处理;如果没有令牌,请求可以选择等待令牌生成或被直接拒绝。桶本身有容量限制,令牌可以在桶中累积,直至达到容量上限。这意味着在流量较低时,桶中可以积攒令牌,允许后续短时间内处理超过平均速率的突发请求,只要累积的令牌足够即可。这使得令牌桶算法在应对流量波动方面比漏桶更具弹性。

滑动窗口算法(Sliding Window Log/Counter) 是对简单计数器算法的优化,旨在解决临界问题并提供更平滑的流量控制。它将时间窗口细分为多个更小的区间(子窗口),并在一个滑动的时间窗口内维护这些小区间的请求计数。随着时间的推移,窗口向前滑动,旧的子窗口被移除,新的子窗口被加入。通过统计当前滑动窗口内所有子窗口的请求总数来判断是否达到限流阈值。这种方式避免了固定窗口切换时的流量突刺,提供了更精确和更平滑的限流效果。

队列机制 也可以用于实现限流。通过将请求放入一个有界队列中,由固定数量的工作线程或进程从队列中取出并处理请求。这种方式可以缓冲突发请求,但它主要是控制并发处理的请求数量,而非严格控制请求速率。如果请求进入队列的速度持续超过处理速度,队列会被填满,导致后续请求被拒绝或响应延迟显著增加。

147. 协程:单进程内实现并发的轻量级方案

协程(Coroutines)是一种比线程更为轻量级的并发执行单元。与由操作系统内核调度的线程不同,协程的调度通常发生在用户空间,由应用程序或运行时环境(如编程语言的异步框架)管理。这使得协程的创建、切换和销毁开销极小。

在单个线程内部,可以同时运行成百上千甚至更多的协程。它们通过协作式调度(cooperative scheduling)或基于事件循环(event loop)的方式进行切换,一个协程主动让出执行权(yield),另一个协程才能获得执行机会。这种方式避免了线程上下文切换的成本,也简化了并发编程中的锁和同步问题(因为在单线程内,同一时间只有一个协程在执行)。协程特别适用于 I/O 密集型任务,能够在等待 I/O 操作完成时切换到其他协程执行,从而提高单线程的资源利用率和程序的并发能力。

148. Java 虚拟机(JVM)中的常见垃圾回收器

Java 虚拟机(JVM)提供了多种垃圾回收器(Garbage Collector, GC),它们采用不同的算法和策略来自动管理内存,回收不再使用的对象。选择合适的 GC 对应用的性能(尤其是吞吐量和停顿时间)至关重要。以下是一些常见的 GC:

  • Serial GC:这是一种单线程执行垃圾回收的收集器。在进行 GC 时,必须暂停所有应用线程(Stop-The-World, STW)。由于其简单和低开销(仅指 GC 自身资源消耗,非停顿时间),适用于客户端模式或内存较小、单核处理器的环境。通过 -XX:+UseSerialGC 启用。
  • Parallel GC(也称为吞吐量优先收集器):这是 JDK 8 及之前版本的默认服务器端 GC。它与 Serial GC 类似,在 GC 时会暂停应用线程,但使用多个线程并行进行垃圾回收,从而缩短 STW 时间,提高回收效率和系统吞吐量。适用于多核服务器环境,对停顿时间要求不高的后台计算任务。通过 -XX:+UseParallelGC 启用。
  • Concurrent Mark Sweep (CMS) GC:以获取最短回收停顿时间为主要目标的收集器。它在垃圾回收的大部分阶段(如并发标记、并发清除)允许应用线程并发执行,显著减少了 STW 时间。但是,它存在一些缺点,如产生内存碎片、对 CPU 资源敏感等。适用于对响应时间有较高要求的应用,如 Web 服务器。通过 -XX:+UseConcMarkSweepGC 启用。注意:CMS 在 Java 9 中被标记为废弃,并在后续版本中移除。

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容

全部评论

相关推荐

评论
3
2
分享

创作者周榜

更多
牛客网
牛客企业服务