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

专栏简介

101. 海量字符串 Top K 频率统计:内存无限与受限场景下的策略

内存无限场景

在不考虑内存限制的情况下,统计大量字符串中出现次数前 10 的字符串,通常采用两阶段策略:

  1. 频率统计:使用哈希映射(如 C++ 的 std::unordered_map 或 Java 的 HashMap)遍历所有字符串。以字符串本身为键(Key),其出现次数为值(Value)。每读取一个字符串,检查其是否已在哈希映射中:若存在,则将其计数值加 1;若不存在,则将其插入映射,计数值设为 1。此过程的时间复杂度为 O(N),其中 N 是字符串总数。
  2. Top K 筛选:统计完成后,需要从哈希映射中找出频率最高的 K 个(本例中 K=10)字符串。可以维护一个大小为 K 的最小堆(Min-Heap)。遍历哈希映射中的每个键值对(字符串及其频率),将其频率与堆顶元素比较:若当前频率大于堆顶频率,则移除堆顶元素,并将当前键值对(或其频率和标识)插入堆中。遍历结束后,堆中存储的就是频率最高的 K 个字符串。此过程的时间复杂度约为 O(N' log K),其中 N' 是不同字符串的数量(即哈希映射的大小)。

整体时间复杂度主要由频率统计阶段的 O(N) 和 Top K 筛选阶段的 O(N' log K) 构成。

内存受限场景

当内存不足以一次性加载所有数据或存储完整的频率映射时,需要采用分而治之(Divide and Conquer)的策略:

  1. 数据分片:选择一个合适的哈希函数,将海量的字符串数据根据哈希值(例如 hash(string) % M)分散到 M 个不同的文件(桶)中。确保每个小文件的大小都能被单机内存处理。相同字符串必然会被哈希到同一个文件中。
  2. 分片处理:对每个小文件,独立应用“内存无限场景”下的策略,即使用哈希映射统计该文件内各字符串的频率,然后使用最小堆找出该文件内的 Top K 字符串及其频率。
  3. 结果合并:收集 M 个小文件产生的 Top K 结果(共 M * K 个候选者)。再次使用一个大小为 K 的最小堆,对这 M * K 个候选者进行最终筛选,找出全局频率最高的 Top K 字符串。

这种方法通过将大问题分解为可在内存中处理的小问题,有效解决了海量数据处理中的内存限制问题。

102. 磁盘快照技术:写时复制 (COW) 与重定向写入 (ROW) 解析

磁盘快照是在特定时间点为文件系统或存储卷创建的一个只读、逻辑副本的技术。它使得用户能够访问、备份或恢复到该时间点的数据状态,而无需中断对原始数据的正常访问,并且通常创建迅速,初始空间占用小。

写时复制 (Copy-On-Write, COW)

COW 是一种常用的快照实现技术。当快照创建后,原始数据块和快照共享物理存储。只有当原始卷中的某个数据块首次需要被修改(写入)时,系统才会先将该原始数据块的内容复制到一个新的位置,然后将修改写入这个新位置。同时,文件系统的元数据会更新,指向这个新写入的数据块。快照则继续引用未被修改的原始数据块。这种方式的优点在于,未被修改的数据块可以在原始卷和多个快照之间共享,从而节省存储空间。缺点是在发生写操作时,需要额外的复制开销。

重定向写入 (Redirect-On-Write, ROW)

ROW 是另一种实现快照的技术。与 COW 不同,当原始卷的数据块需要被修改时,ROW 直接将新的数据写入到一个预留的、不同于原始位置的新块中,并更新元数据指向新块。原始数据块的内容保持不变,供快照使用。这意味着每次写操作都会分配新的存储空间,而不会有 COW 那样的首次写入复制开销。相比 COW,ROW 对于原始卷的写入性能可能更稳定,但随着时间的推移和修改量的增加,可能会更快地消耗存储空间,因为修改总是导致新块的分配,而不是潜在地覆盖旧的、只被快照引用的“副本”。

简单类比:

  • COW 像是在读书笔记旁做标注:你有一本书(原始数据),想保留某个时刻的状态(创建快照)。之后,当你想在某页做笔记(修改数据)时,你先复印那一页(复制原始块),然后在复印件上做笔记(写入新块),原始书页保持不变(供快照读取)。没做笔记的书页,你和快照看的是同一份原版。
  • ROW 像是在旁边的新纸上写更新:你有一本书(原始数据),想保留状态(创建快照)。当你想更新某页内容时,你在旁边拿一张新纸(新块),写上更新后的内容,并在书的目录(元数据)里标记,这一页的最新内容在新纸上。原始书页保持不变(供快照读取)。每次更新都用新纸。

103. 单核 CPU 环境下进程死循环的影响与解决策略

影响分析

在只有一个 CPU 核心的操作系统(如早期 Linux 或特定配置环境)中,如果一个进程陷入死循环,它并不会直接导致其他进程完全停止运行。现代操作系统普遍采用抢占式多任务调度机制。调度器会为每个就绪进程分配一个时间片(time slice)。当死循环进程获得 CPU 时间片时,它会持续执行循环,耗尽整个时间片,表现为 CPU 使用率接近 100%。

当该进程的时间片用完后,调度器会剥夺其 CPU 使用权,并切换到下一个就绪进程。因此,其他进程仍然有机会获得 CPU 并执行。然而,由于死循环进程频繁且持续地占用 CPU 资源,会导致:

  1. 系统整体性能严重下降:其他进程能分到的 CPU 时间总量减少,响应速度变慢,用户会感觉系统卡顿。
  2. 资源饥饿:如果死循环进程在循环中持有某些共享资源(如锁、文件句柄),可能导致其他需要这些资源的进程长时间等待,甚至饿死。

解决策略

针对进程死循环问题,可以采取以下措施:

  1. 强制终止进程:最直接的方法是使用操作系统提供的工具(如 Linux 的 kill 命令或任务管理器)识别并终止该死循环进程。这能立即释放 CPU 资源,恢复系统正常运行。
  2. 代码调试与修复:从根本上解决问题需要定位到导致死循环的源代码,分析循环条件、退出逻辑或状态变量是否存在错误,并进行修复。这是预防未来再次发生的关键。
  3. 资源限制与优先级调整:可以通过操作系统机制限制问题进程的资源使用。例如,在 Linux 中使用 nice 命令降低其调度优先级,或使用 cgroups (Control Groups) 限制其 CPU 使用配额。这可以在无法立即修复代码时,减轻其对系统的影响。
  4. 系统监控与告警:部署系统监控工具,实时监测各进程的 CPU 使用率。设置告警阈值,当某个进程 CPU 使用率持续过高时自动发出警报,以便管理员及时介入处理。
  5. 看门狗 (Watchdog) 机制:在程序内部或外部署看门狗。死循环进程若无法在规定时间内“喂狗”(表明其仍在正常运行),看门狗会触发预设操作,如重启进程或整个系统。

虽然问题设定在单核环境,但在多核环境下,一个进程死循环通常只会占满一个核心,对其他核心上运行的进程影响相对较小,但仍需处理。

104. 常见网络攻击类型及项目中的防御策略

网络攻击是信息系统面临的持续威胁,了解常见的攻击方式并采取相应的防御措施至关重要。以下是一些典型的网络攻击及其在项目中的常规防范策略:

拒绝服务攻击 (DoS/DDoS)此类攻击通过发送大量无效或高负荷请求,耗尽目标服务器的网络带宽、连接数或计算资源,使其无法响应正常用户的请求。

  • 防御策略:使用内容分发网络 (CDN) 分散流量,吸收部分攻击流量;部署专业的 DDoS 防护服务(如云 WAF、流量清洗中心);在服务器和网络边界配置防火墙、入侵检测/防御系统 (IDS/IPS) 实施流量速率限制和异常连接阻断。

SQL 注入 (SQL Injection)攻击者在用户输入(如表单、URL 参数)中插入恶意的 SQL 代码片段,若应用程序未充分过滤或转义输入,这些代码可能被后端数据库执行,导致数据泄露、篡改或系统被控制。

  • 防御策略严禁直接拼接 SQL 查询字符串。优先使用参数化查询(Prepared Statements)或存储过程;采用 ORM (Object-Relational Mapping) 框架,它们通常内置了对 SQL 注入的防护;对所有用户输入进行严格的验证、过滤和转义。

跨站脚本攻击 (XSS)攻击者将恶意脚本(通常是 JavaScript)注入到受信任的网站页面中。当其他用户访问该页面时,恶意脚本会在其浏览器中执行,可能窃取 Cookie、会话令牌,或执行其他恶意操作。

  • 防御策略:对所有用户输入以及输出到 HTML 页面的内容进行严格的上下文相关的编码或转义(如 HTML 实体编码);设置 HttpOnly 属性保护 Cookie;实施内容安全策略 (CSP) 限制浏览器可加载和执行的资源来源。

跨站请求伪造 (CSRF)攻击者诱导已登录用户在不知情的情况下,点击恶意链接或访问包含恶意代码的页面,从而以用户的身份向目标网站发送非预期的、可能有害的请求(如修改密码、转账等)。

  • 防御策略:在表单中使用并验证 CSRF 令牌(Anti-CSRF Token);检查 HTTP 请求的 Referer 头部(有一定局限性);使用 SameSite Cookie 属性限制第三方 Cookie 发送。

中间人攻击 (MITM)攻击者在通信双方之间秘密拦截、窃听甚至篡改通信内容。

  • 防御策略:全站强制使用 HTTPS (HTTP over TLS/SSL) 对传输数据进行加密;验证服务器证书的有效性;在内部网络或敏感通信场景使用 VPN (Virtual Private Network)。

此外,还需关注钓鱼攻击(通过用户教育和邮件过滤防御)、零日漏洞攻击(通过及时更新系统补丁、使用 WAF 和 IDS/IPS 防御)等其他威胁,并建立完善的安全审计和应急响应机制。

105. MySQL InnoDB 行锁机制与显式锁定语法

MySQL 的 InnoDB 存储引擎支持行级锁定(Row-Level Locking),相比表级锁定提供了更高的并发性能,允许多个事务同时修改同一张表的不同行。

隐式行锁在事务中,当你执行 写操作(如 INSERT, UPDATE, DELETE)时,InnoDB 会自动为涉及的数据行添加排他锁(Exclusive Lock,X锁)。这确保了事务的 ACID 属性,防止其他事务同时修改这些行。标准的 SELECT 读操作默认情况下是非锁定读(使用 MVCC,多版本并发控制),不会加锁,也不会被行锁阻塞(除非隔离级别很高或遇到特定情况)。

显式行锁语法有时,需要在事务中显式地锁定某些行,以确保读取数据后到更新或删除操作完成前,这些行不被其他事务修改。InnoDB 提供了两种主要的显式行锁语句:

  1. 排他锁 (Exclusive Lock): 使用 SELECT ... FOR UPDATE此语句会对查询结果集中的每一行添加排他锁(X锁)。一旦某行被加上 X 锁,其他事务不能再对该行加任何类型的锁(无论是共享锁还是排他锁),也不能修改或删除该行,直到持有锁的事务提交或回滚。需要修改数据的场景通常使用此锁。
  2. 共享锁 (Shared Lock): 使用 SELECT ... LOCK IN SHARE MODE (或 MySQL 8.0+ 的 SELECT ... FOR SHARE)此语句会对查询结果集中的每一行添加共享锁(S锁)。多个事务可以同时持有同一行的共享锁,意味着它们都可以读取该行。但是,任何事务(包括持有 S 锁的事务)都不能对该行进行修改(即不能获取 X 锁),直到所有 S 锁被释放。适用于读取数据并希望确保在事务期间数据不被修改,但允许其他事务也读取的场景。

无论是隐式锁还是显式锁,行锁都是在事务范围内有效的,锁会一直持有直到事务结束(执行 COMMIT 或 ROLLBACK)。不恰当或过长时间持有行锁可能导致锁竞争,降低并发性能,甚至引发死锁。

106. MySQL 死锁排查与解决策略

MySQL 数据库死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象。有效排查和解决死锁对于保障数据库稳定性和性能至关重要。

排查死锁

  1. 查看死锁日志: 最有效的方法是启用 InnoDB 的死锁日志记录功能。通过设置 innodb_print_all_deadlocks = 1(可动态设置 SET GLOBAL innodb_print_all_deadlocks = 1; 或在配置文件中永久设置),MySQL 会在发生死锁时,将详细的死锁信息(包括涉及的事务、等待的锁、持有的锁、执行的 SQL 语句等)输出到 MySQL 的错误日志(error log)中。这是分析死锁根本原因的首选方式。
  2. 实时状态监控: 使用 SHOW ENGINE INNODB STATUS 命令可以获取 InnoDB 存储引擎的详细状态,其输出内容中包含 LATEST DETECTED DEADLOCK 部分,会显示最近一次死锁的详细信息。这对于快速查看刚发生的死锁很有帮助。
  3. 查询锁信息表: INFORMATION_SCHEMA 库中的 INNODB_LOCKS 表展示了当前持有的锁,而 INNODB_LOCK_WAITS 表则显示了哪些事务正在等待锁以及它们被哪个事务阻塞。通过查询这两个表,可以实时了解当前的锁竞争情况,辅助判断是否存在死锁或潜在的死锁风险。
  4. 检查活跃进程: SHOW PROCESSLIST 命令可以列出当前所有连接的线程状态,可以帮助识别长时间处于 query 状态或等待锁的线程,结合其他信息排查问题。

解决与预防死锁

  1. InnoDB 自动处理: InnoDB 存储引擎内置了死锁检测机制(默认开启)。一旦检测到死锁,它会自动选择一个或多个事务进行回滚(通常选择回滚代价最小的事务),以打破死锁循环,让其他事务得以继续执行。
  2. 手动终止事务: 如果需要立即解决死锁且自动处理机制尚未生效或选择了不期望回滚的事务,可以通过 SHOW PROCESSLIST 找到死锁事务的线程 ID (Id 列),然后使用 KILL <thread_id> 命令手动终止其中一个事务。
  3. 优化应用程序逻辑:减少事务持有锁的时间: 尽量缩短事务的执行时间,将耗时操作(如复杂的计算、外部调用)移出事务边界。保持一致的访问顺序: 确保不同的事务在访问多个资源时,总是以相同的顺序请求锁。例如,总是先锁表 A 再锁表 B。减小事务粒度: 将大的事务拆分成若干个小事务,降低锁冲突的概率。优化 SQL 与索引: 确保查询能高效利用索引,减少扫描的行数,从而减少需要锁定的资源范围和时间。避免全表扫描或不必要的行锁。使用更低隔离级别: 在业务允许的情况下,考虑使用如 READ COMMITTED 的隔离级别,相比 REPEATABLE READ,它通常能减少间隙锁的使用,降低死锁风险。显式锁定需谨慎: 合理使用 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE,仅在必要时锁定记录,并尽快释放。

107. Java 中重写 equals 方法:以包含 String 和 Integer 的类为例

在 Java 中,重写 equals 方法是为了定义对象实例之间逻辑相等的标准。当默认的 Object.equals() 方法(比较对象内存地址)不适用时,需要根据类的业务含义来自定义相等性判断。对于一个包含 String 和 Integer 成员变量的类 ClassA,正确的 equals 方法实现应遵循以下原则和步骤:

  1. 使用 @Override 注解: 明确表示该方法意图覆盖父类(Object)的方法,编译器会检查方法签名是否正确。
  2. 检查同一性: 使用 this == obj 判断传入对象是否与当前对象是同一个内存实例。如果是,则它们必然相等,直接返回 true,这是最高效的判断。
  3. 检查类型兼容性: 使用 instanceof 操作符或 obj.getClass() == this.getClass()(取决于是否允许子类比较)判断传入对象 obj 是否为 ClassA 类型或其兼容类型。如果类型不匹配,则不能相等,返回 false。注意对 obj 为 null 的情况,instanceof 会直接返回 false,无需单独的 null 检查。
  4. 类型转换: 将 obj 强制类型转换为 ClassA 类型,以便访问其成员变量。
  5. 比较关键字段: 逐一比较决定对象相等性的所有关键字段。对于基本类型字段,使用 == 比较。对于对象类型字段(如 String, Integer),必须使用其自身的 equals 方法进行比较,并且必须处理字段可能为 null 的情况,以避免 NullPointerException。常用的 null 安全比较方式是 Objects.equals(field1, other.field1) 或 手动检查 (field1 == null ? other.field1 == null : field1.equals(other.field1))。

以下是 ClassA 的 equals 方法示例:

import java.util.Objects;

public class ClassA {
    private String str;
    private Integer num;

    // 构造函数、getters 和 setters 省略...

    @Override
    public boolean equals(Object obj) {
        // 1. 检查同一性
        if (this == obj) {
            return true;
        }
        // 2. 检查类型兼容性 (包含 null 检查)
        if (!(obj instanceof ClassA)) {
            return false;
        }
        // 3. 类型转换
        ClassA other = (ClassA) obj;
        
        // 4. 比较关键字段 (使用 Objects.equals 处理 null)
        return Objects.equals(this.str, other.str) &&
               Objects.equals(this.num, other.num);
    }

    // 重要提示:重写 equals 方法通常也需要重写 hashCode 方法,以维护其约定。
    @Override
    public int hashCode() {
        return Objects.hash(str, num);
    }
}

注意: 根据 equals 方法的约定,如果重写了 equals,也必须重写 hashCode 方法,保证相等的对象具有相同的哈希码。

108. Spring 事务传播机制详解

Spring 框架提供了强大的声明式事务管理功能,其中事务传播(Transaction Propagation)机制定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播或管理。这允许开发者精细控制事务边界和行为。

Spring 定义了以下几种主要的事务传播行为:

  • REQUIRED (默认): 如果当前存在一个事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的传播行为,适合绝大多数需要事务支持的场景。
  • SUPPORTS: 如果当前存在一个事务,则加入该事务;如果当前没有事务,则以非事务方式执行。适用于那些方法本身不需要事务,但如果被其他事务性方法调用时,可以参与到现有事务中的场景。
  • MANDATORY: 强制要求当前必须存在一个事务。如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。适用于必须在事务环境下执行的方法。
  • REQUIRES_NEW: 总是创建一个新的事务。如果当前存在事务,则将当前事务挂起(pause),执行新事务,待新事务完成后,再恢复(resume)被挂起的事务。适用于需要独立事务保证的操作,其结果不应受外部事务回滚的影响。
  • NOT_SUPPORTED: 以非事务方式执行操作。如果当前存在事务,则将当前事务挂起,执行非事务操作,完成后再恢复事务。适用于明确不需要事务支持的方法,即使它被一个事务性方法调用。
  • NEVER: 强制要求以非事务方式执行。如果当前存在事务,则抛出异常。适用于那些绝对不允许在事务中运行的方法。
  • NESTED: 如果当前存在一个事务,则创建一个嵌套事务(savepoint)。嵌套事务是外部事务的一部分,它有自己的保存点。如果嵌套事务回滚,只会回滚到其保存点,外部事务可以选择继续执行或整体回滚。如果当前没有事务,则行为类似于 REQUIRED,创建一个新事务。NESTED 通常需要底层数据库(如 JDBC 3.0+)支持保存点。

通过在 @Transactional 注解中使用 propagation 属性(例如 @Transactional

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

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

全部评论

相关推荐

评论
5
1
分享

创作者周榜

更多
牛客网
牛客企业服务