Skip to content

校招后端面经系列--腾讯会议一面,面试刚结束,流程就结束了。。。 ​

来源:https://www.nowcoder.com/feed/main/detail/3f91ec18b3544a44a3310ab6f083f1bf

1、线程池 七个参数 如果任务队列无界的话 则最大线程数还用得到吗? ​

线程池的七个参数 ​

  1. corePoolSize(核心线程数):线程池在保持活动状态的最小线程数,即使它们没有任务可执行。
  2. maximumPoolSize(最大线程数):线程池允许的最大线程数。
  3. keepAliveTime(线程空闲保持时间):当线程数超过核心线程数时,空闲线程在终止前等待新任务的最长时间。
  4. unit(时间单位):keepAliveTime 参数的时间单位。
  5. workQueue(任务队列):用于保存等待执行任务的队列。
  6. threadFactory(线程工厂):用于创建新线程的工厂。
  7. handler(拒绝策略):当任务队列已满并且线程数达到最大值时如何处理新任务。

无界任务队列与最大线程数 ​

当任务队列是无界时,意味着任务队列可以无限增长,不会因为队列满而拒绝任务。但这并不意味着最大线程数失去了意义。原因:

  1. 控制并发量:最大线程数用于限制可以同时执行的任务数量,防止系统资源被过度消耗。如果没有最大线程数限制,线程池可能会无限制地创建新线程,这会导致系统资源耗尽,产生OOM(OutOfMemory)错误或其他性能问题。
  2. 避免资源竞争:通过限制最大线程数,可以避免过多线程争夺系统资源(如CPU、内存、I/O),从而提高系统的整体性能和稳定性。
  3. 应对突发任务:即使任务队列无界,当大量任务突然到来时,最大线程数限制了同时执行任务的数量,剩余任务将在队列中等待。这样可以防止瞬时高负载对系统造成冲击。
  4. 系统负载管理:在高负载情况下,通过控制最大线程数,可以更好地管理系统负载,确保系统在高峰期间仍然可以平稳运行。

即使任务队列是无界的,最大线程数仍然是必要的,因为它限制了线程池中同时执行的线程数量,从而控制系统资源的使用,避免系统资源被过度消耗。

2、Java内存模型? ​

用于定义多线程环境下,Java 程序中变量的访问规则以及不同线程之间的变量可见性和一致性。

  1. 主内存和工作内存:
    • 主内存:所有的实例变量、静态变量和数组元素都存储在主内存中。
    • 工作内存:每个线程都有自己的工作内存,工作内存保存了该线程使用到的变量的副本。线程对变量的所有操作(读取和写入)都必须在工作内存中进行,而不是直接在主内存中进行。
  2. 同步规则:
    • volatile 变量:当一个变量被声明为 volatile,它会确保对该变量的修改对所有线程是可见的。具体而言,读取 volatile 变量时会直接从主内存读取,而写入 volatile 变量时会直接写入主内存。
    • synchronized 块:synchronized 关键字用于确保某段代码在同一时间只能被一个线程执行。进入 synchronized 块之前,线程会获取锁,并且从主内存中刷新变量到工作内存。退出 synchronized 块时,线程会释放锁,并将工作内存中的变量写回主内存。
  3. 内存栅栏:
    • 内存栅栏是一个CPU指令,用于限制特定的内存操作的重排序。Java 提供了一些方法如 volatile 变量和 synchronized 块来隐式使用内存栅栏,保证内存操作的可见性和顺序性。

JMM中的原则 ​

  1. 可见性:

    当一个线程修改了一个共享变量,其它线程能够立即看到这个修改。volatile 变量和 synchronized 块可以保证可见性。

  2. 原子性:

    原子性确保一个操作或多操作序列的不可分割性,即操作要么全部执行,要么全部不执行。Java 中对基本数据类型的读写是原子的,而 long 和 double 类型的读写在某些平台下不是原子的,除非使用 volatile 关键字。

  3. 有序性:

    有序性是指程序中指令的执行顺序。JMM 提供了一些方法来控制指令的重排序,例如 volatile 变量和 synchronized 块。

3、类加载过程? ​

Java 中,类加载过程是指将类的字节码加载到内存中,并对其进行验证、准备、解析和初始化的过程。

  1. 加载 (Loading):

    • 查找并加载类的二进制数据。

    • 创建一个 Class 对象,用来封装类在方法区中的数据结构。

    • 这个阶段还会检查类文件的格式是否符合规范。

  2. 验证 (Verification):

    • 确保加载的类的字节码符合 JVM 规范,并且不会危害虚拟机。

    • 验证阶段包括文件格式验证、元数据验证、字节码验证和符号引用验证。

  3. 准备 (Preparation):

    • 为类的静态变量分配内存,并将其初始化为默认值(默认值即该类型的默认值,如 int 为 0,引用类型为 null)。
  4. 解析 (Resolution):

    • 将常量池中的符号引用替换为直接引用。

    • 符号引用是用一组符号来描述目标,可以是类名、方法名等;直接引用就是指向目标的指针、偏移量等。

  5. 初始化 (Initialization):

    • 初始化阶段是执行类构造器 <clinit>() 方法的过程。

    • 类构造器 <clinit>() 方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块(static {})中的语句合并产生的。

    • 初始化是类加载过程的最后一步。

4、新生代(Ed、FS、TS)、老生代(垃圾回收:CMS)? ​

在 Java 虚拟机 (JVM) 的内存管理中,堆内存被划分为不同的区域以便更有效地进行垃圾回收。主要包括新生代 (Young Generation) 和老生代 (Old Generation) 这两个区域。

新生代 (Young Generation) ​

新生代主要用来存放新创建的对象。由于对象大多具有较短的生命周期,新生代的垃圾回收频率较高。新生代进一步分为以下几个部分:

  1. Eden 区 (Eden Space):
    • 新生代中的主要区域,新创建的对象首先分配在这里。
    • 当 Eden 区满时,会触发一次 Minor GC(小型垃圾回收)。
  2. From Survivor 区 (Survivor Space 0):
    • 作为对象的过渡存储区域。
    • Minor GC 时,Eden 区和 From Survivor 区中的存活对象会被复制到 To Survivor 区。
  3. To Survivor 区 (Survivor Space 1):
    • 和 From Survivor 区的角色在每次 Minor GC 后互换。
    • 两个 Survivor 区中的对象会在 Minor GC 过程中互相复制,直到达到一定的年龄后晋升到老生代。

老生代 (Old Generation) ​

老生代存储生命周期较长的对象。经过多次 Minor GC 仍存活的对象会从新生代晋升到老生代。老生代的垃圾回收频率较低,但回收时间较长。主要的垃圾回收器之一是 CMS (Concurrent Mark-Sweep)。

CMS 垃圾回收器 ​

CMS (Concurrent Mark-Sweep) 是一种以获得最短回收停顿时间为目标的垃圾回收算法。CMS 主要用于老生代的垃圾回收,具体步骤如下:

  1. 初始标记 (Initial Mark):
    • 标记从根对象 (GC Roots) 可达的对象。
    • 这个阶段会暂停所有的应用线程 (Stop-The-World)。
  2. 并发标记 (Concurrent Mark):
    • 对初始标记阶段标记的对象进行进一步标记,找出所有可达的对象。
    • 这个阶段与应用线程并发执行,不会暂停应用线程。
  3. 重新标记 (Remark):
    • 修正并发标记阶段期间应用线程继续运行而导致的标记变动。
    • 这个阶段会再次暂停所有的应用线程 (Stop-The-World)。
  4. 并发清除 (Concurrent Sweep):
    • 清除未被标记的对象,回收空间。
    • 这个阶段与应用线程并发执行,不会暂停应用线程。

5、mysql的存储引擎 (innodb)? ​

1. InnoDB ​

InnoDB 是 MySQL 的默认存储引擎,支持事务、安全性和高并发性。其特点包括:

  • 事务支持:支持 ACID(原子性、一致性、隔离性、持久性)事务特性。
  • 外键支持:支持外键约束,确保数据的完整性和一致性。
  • 自动崩溃恢复:在数据库崩溃后,自动恢复未完成的事务。
  • 行级锁定:提供高并发性,避免表级锁定引起的性能瓶颈。
  • MVCC:多版本并发控制 (MVCC) 支持高并发性读写操作。

2. MyISAM ​

MyISAM 是早期 MySQL 的默认存储引擎,具有以下特点:

  • 不支持事务:不支持事务和外键。
  • 表级锁定:在写操作时锁定整个表,导致并发性较低。
  • 高效的读操作:适用于读多写少的应用场景。
  • 压缩表:支持表压缩,减少磁盘空间使用。

3. Memory ​

Memory 存储引擎将数据存储在内存中,适用于需要高速访问的临时数据。其特点包括:

  • 高速访问:由于数据存储在内存中,读写速度非常快。
  • 非持久性:数据在服务器重启时会丢失。
  • 表级锁定:不支持行级锁定。

4. CSV ​

CSV 存储引擎将数据存储为 CSV 文件,适用于数据交换。其特点包括:

  • 简单易用:数据格式为 CSV 文件,易于导入和导出。
  • 无索引支持:不支持索引,查询性能较低。
  • 适用场景有限:适用于简单的数据交换和备份。

5. Archive ​

Archive 存储引擎用于存储大批量的历史归档数据。其特点包括:

  • 高压缩比:数据以高压缩比存储,节省磁盘空间。
  • 仅支持插入和查询:不支持更新和删除操作。
  • 适用于归档数据:适用于存储日志、历史数据等。

6、mysql 的主键索引 和 唯一索引 (覆盖索引)? ​

主键索引 (Primary Key Index) ​

特点 ​
  1. 唯一性:主键索引保证列中的每个值都是唯一的,不允许重复。
  2. 非空性:主键列不能包含 NULL 值,必须有实际数据。
  3. 自动创建索引:定义主键时,MySQL 会自动为该列创建一个唯一的 B-Tree 索引。
  4. 表的标识:每个表只能有一个主键,作为表中每行记录的唯一标识。
  5. 聚集索引 (Clustered Index):InnoDB 存储引擎中,主键索引是聚集索引,数据按主键的顺序物理存储在磁盘上。
创建示例 ​
sql
CREATE TABLE example_table (
    id INT AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    PRIMARY KEY (id)
);

唯一索引 (Unique Index) ​

特点 ​
  1. 唯一性:唯一索引保证列中的每个值都是唯一的,但允许包含一个或多个 NULL 值。
  2. 可多个:一个表可以有多个唯一索引,每个唯一索引都能保证其列数据的唯一性。
  3. 非聚集索引 (Non-Clustered Index):唯一索引通常是非聚集索引,不会影响数据在磁盘上的物理存储顺序。
创建示例 ​
sql
CREATE TABLE example_table (
    id INT AUTO_INCREMENT,
    email VARCHAR(255) NOT NULL,
    UNIQUE (email)
);

7、索引的数据结构 hash 平衡树 B+树 B树? ​

1. 哈希 (Hash) ​

特点 ​
  • O(1) 时间复杂度:哈希索引能够在 O(1) 时间内进行查找,非常高效。
  • 适用于等值查询:哈希索引非常适合用于等值查询(如 =、IN),不适用于范围查询(如 <、>)。
  • 无序存储:数据在哈希表中无序存储,不支持顺序访问。
  • 冲突处理:哈希索引需要处理哈希冲突,常见的方法包括链地址法和开放地址法。
使用场景 ​

用于等值查询频繁的场景,如精确匹配查找。

示例 ​

哈希索引在 MySQL 的 Memory 存储引擎中得到应用。

2. 平衡树 (Balanced Tree) ​

平衡树是一种保持自平衡的二叉搜索树,其子树高度差最多为 1,确保查找、插入和删除操作的时间复杂度为 O(log n)。

使用场景 ​

较少用于实际数据库索引结构,因为更复杂的数据结构如 B-Tree 和 B+ Tree 提供了更高效的磁盘读写性能。

3. B树 (B-Tree) ​

B树是一种多路自平衡搜索树,它保持数据排序,并允许高效的顺序访问、插入、删除和查找操作。

特点 ​
  • 多路搜索树:每个节点可以有多个子节点,节点包含多个键值。
  • 平衡性:所有叶子节点在同一层,确保查找路径长度一致。
  • 适用于磁盘存储:B树节点通常与磁盘页大小一致,以优化磁盘读写效率。
使用场景 ​

适用于文件系统和数据库系统中的索引。

4. B+树 (B+ Tree) ​

B+树是 B树的变种,广泛应用于数据库索引。与 B树相比,B+树具有以下特点:

特点 ​
  • 叶子节点链表:所有数据都存储在叶子节点,并通过链表连接,便于顺序访问。
  • 非叶子节点仅存储键值:内部节点只存储键值及指向子节点的指针,不存储实际数据,能够存储更多的键值,减少树的高度。
  • 范围查询高效:由于叶子节点按顺序链接,B+树适合范围查询。
使用场景 ​

广泛用于数据库系统和文件系统的索引结构,适用于范围查询和顺序访问。

示例 ​

MySQL 的 InnoDB 存储引擎使用 B+树作为默认的索引结构。

数据结构比较 ​

数据结构时间复杂度存储顺序优点缺点适用场景
哈希 (Hash)O(1)无序查找速度快不支持范围查询等值查询
平衡树 (Balanced Tree)O(log n)有序保持平衡复杂性高较少用于数据库索引
B树 (B-Tree)O(log n)有序高效磁盘读写结构复杂数据库索引
B+树 (B+ Tree)O(log n)有序高效范围查询结构复杂数据库索引

8、HTTP2 与 HTTP3区别? ​

基础概念 ​

HTTP/2 ​
  • 发布年份:2015
  • 底层协议:TCP (Transmission Control Protocol)
  • 主要特点:
    • 二进制分帧:HTTP/2 使用二进制格式而不是文本格式进行数据传输,这提高了传输效率。
    • 多路复用:允许多个请求和响应通过单个 TCP 连接同时发送,避免了 HTTP/1.1 中的队头阻塞(Head-of-Line Blocking)。
    • 头部压缩:使用 HPACK 算法对头部信息进行压缩,减少了头部开销。
    • 服务器推送:服务器可以主动向客户端推送资源,而不需要客户端显式请求。
HTTP/3 ​
  • 发布年份:2020
  • 底层协议:QUIC (Quick UDP Internet Connections)
  • 主要特点:
    • 基于 UDP:HTTP/3 使用 QUIC 作为传输协议,基于 UDP 而非 TCP,解决了 TCP 的一些固有问题。
    • 集成加密:QUIC 本身集成了 TLS 1.3,加密传输数据,提高了安全性。
    • 更快的连接建立:QUIC 使用 0-RTT 和 1-RTT 握手,可以显著减少连接建立的延迟。
    • 移动友好:QUIC 连接能够在网络变化(如从 WiFi 切换到蜂窝网络)时保持连接,减少连接中断。

具体区别 ​

1. 传输协议 ​
  • HTTP/2:基于 TCP。
  • HTTP/3:基于 QUIC(UDP)。
2. 连接建立 ​
  • HTTP/2:使用 TCP 三次握手和 TLS 握手,通常需要 2-3 个往返时间(RTT)。
  • HTTP/3:使用 QUIC 的 0-RTT 和 1-RTT 握手,可以在一个 RTT 内建立连接。
3. 多路复用 ​
  • HTTP/2:多路复用在 TCP 连接之上,但如果一个流发生丢包,整个连接都会受到影响(TCP 的队头阻塞问题)。
  • HTTP/3:多路复用在 QUIC 连接之上,每个流都是独立的,单个流的丢包不会影响其他流,避免了队头阻塞。
4. 安全性 ​
  • HTTP/2:通常与 TLS 一起使用,但 TLS 并不是协议的一部分。
  • HTTP/3:QUIC 本身就包含了 TLS 1.3 的加密功能。
5. 头部压缩 ​
  • HTTP/2:使用 HPACK 算法进行头部压缩。
  • HTTP/3:使用 QPACK 算法进行头部压缩,改进了 HPACK,以更好地适应 QUIC 的多路复用特性。
6. 网络切换 ​
  • HTTP/2:TCP 连接在网络变化时容易中断,需要重新建立连接。
  • HTTP/3:QUIC 连接在网络变化时可以保持连接,不需要重新建立连接。

9、JVM锁机制 怎么样判断一个对象有没有被锁住? ​

使用 synchronized 关键字的锁机制 ​

在 Java 中,synchronized 可以用于方法和代码块。锁住一个对象(或类)的关键方法如下:

  1. 同步方法:在实例方法上使用 synchronized 关键字。
  2. 同步代码块:在方法内部使用 synchronized(obj) 来锁住指定对象。

如何判断一个对象是否被锁住? ​

判断一个对象是否被锁住并没有直接的 API,但可以通过以下方式间接判断:

1. 使用线程转储(Thread Dump) ​

线程转储可以显示当前 JVM 中所有线程的状态以及它们持有或等待的锁。可以通过以下步骤生成线程转储:

  • 在 Linux/Unix 系统中,可以使用 kill -3 <pid> 命令。
  • 在 Windows 系统中,可以使用 jstack <pid> 命令。
  • 通过 JVM 自带工具如 JVisualVM、JConsole 也可以生成线程转储。
2. 使用监视工具 ​

一些监视工具如 JVisualVM、JConsole 和第三方 APM(应用性能管理)工具可以显示锁的状态和线程状态。通过这些工具可以查看 JVM 中的锁和线程活动,帮助判断对象是否被锁住。

3. 编程方式 ​

通过编程方式判断某个对象是否被锁住比较复杂,通常涉及到使用 java.util.concurrent.locks 包中的锁类。

使用 java.util.concurrent 包中的锁 ​

java.util.concurrent.locks 包中的锁提供了更多控制和状态查询方法,例如:

  • ReentrantLock:可重入锁,支持公平锁和非公平锁。
  • ReadWriteLock:读写锁,允许多个读锁和一个写锁。

这些锁提供了更多的方法来检查锁的状态,例如 isLocked() 和 isHeldByCurrentThread()。

10、undolog binlog redolog是干啥的 执行顺序? ​

1. Undo Log ​

功能:

  • 回滚:undo log 记录事务中的每一步操作,能够撤销已经执行的操作,使数据回滚到之前的状态。主要用于支持事务的原子性(Atomicity)。
  • 一致性读:在多版本并发控制(MVCC)中,undo log 帮助提供一致性读(Consistent Read),即读操作能够看到事务开始时的数据状态,而不受其他事务的影响。

执行顺序:

在执行数据修改操作之前,undo log 先记录修改前的数据,以便在需要回滚时恢复。

2. Redo Log ​

功能:

  • 持久性:redo log 记录了数据修改操作,确保在发生崩溃时可以将数据库恢复到崩溃前的状态。它保证了事务的持久性(Durability)。
  • 数据恢复:在崩溃恢复时,redo log 用于重做已提交事务的操作,以确保数据库的一致性。

执行顺序:

在事务提交之前,redo log 会先将数据修改操作记录到日志文件中(通常是内存中的日志缓冲区),然后再将这些日志刷入磁盘(称为 WAL,Write-Ahead Logging)。事务提交时,必须确保相关的 redo log 已经写入磁盘。

3. Binlog (Binary Log) ​

功能:

  • 复制:binlog 主要用于主从复制(Master-Slave Replication),记录了所有更改数据库状态的操作。通过将 binlog 发送到从库,可以在从库上重放这些操作,保持主从库的数据一致。
  • 审计和恢复:binlog 也可用于审计数据库的所有修改操作,并在需要时用于数据恢复。

执行顺序:

在事务提交后,binlog 会记录该事务的操作。与 redo log 不同,binlog 记录的通常是 SQL 语句或行级的修改,而 redo log 记录的是物理层面的数据页修改。

执行顺序和关系 ​

  1. 事务开始:当一个事务开始时,会记录相应的 undo log 信息,以便在事务失败时回滚操作。
  2. 执行修改:在执行数据修改操作时,记录 undo log 信息。
  3. 记录 redo log:在修改操作即将提交时,将相应的修改记录到 redo log 中。
  4. 写入 redo log:将 redo log 写入磁盘,确保日志持久化(WAL 机制)。
  5. 提交事务:事务提交,意味着数据修改可以永久保存。
  6. 记录 binlog:事务提交成功后,将相应的修改记录到 binlog 中。

算法:买卖股票 ​

思路 ​

  1. 初始化 min_price 为第一天的股票价格 prices[0],并将 max_profit 设为0。
  2. 从第二天的价格 prices[1] 开始遍历数组,依次执行以下操作:
    1. 如果当前价格 prices[i] 小于 min_price,则将 min_price 更新为 prices[i]。
    2. 否则,计算当前价格 prices[i] 减去 min_price 的差值,如果这个差值大于 max_profit,则将 max_profit 更新为这个差值。
  3. 遍历完成后,max_profit 就是所能获取的最大利润。

参考代码 ​

Java ​
java
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] prices = {7, 1, 5, 3, 6, 4};
        int result = maxProfit(prices);
        System.out.println("Max Profit: " + result);
    }

    /**
     * 计算股票的最大利润
     *
     * @param prices 股票价格数组
     * @return 最大利润
     */
    public static int maxProfit(int[] prices) {
        // 如果价格数组为空,返回0
        if (prices.length == 0) {
            return 0;
        }

        // 初始化最低价格为第一天的价格
        int minPrice = prices[0];
        // 初始化最大利润为0
        int maxProfit = 0;

        // 遍历价格数组,从第二天开始计算最大利润
        for (int i = 1; i < prices.length; i++) {
            // 如果当前价格小于最低价格,则更新最低价格
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            } else {
                // 否则,计算当前利润
                int currentProfit = prices[i] - minPrice;
                // 如果当前利润大于最大利润,则更新最大利润
                if (currentProfit > maxProfit) {
                    maxProfit = currentProfit;
                }
            }
        }

        return maxProfit;
    }
}
C++ ​
cpp
#include <vector>
#include <iostream>

/**
 * @brief 计算股票的最大利润
 * 
 * @param prices 股票价格数组
 * @return int 最大利润
 */
int maxProfit(std::vector<int>& prices) {
    // 如果价格数组为空,返回0
    if (prices.empty()) {
        return 0;
    }

    // 初始化最低价格为第一天的价格
    int min_price = prices[0];
    // 初始化最大利润为0
    int max_profit = 0;

    // 遍历价格数组,从第二天开始计算最大利润
    for (int i = 1; i < prices.size(); i++) {
        // 如果当前价格小于最低价格,则更新最低价格
        if (prices[i] < min_price) {
            min_price = prices[i];
        } else {
            // 否则,计算当前利润
            int current_profit = prices[i] - min_price;
            // 如果当前利润大于最大利润,则更新最大利润
            if (current_profit > max_profit) {
                max_profit = current_profit;
            }
        }
    }

    return max_profit;
}

int main() {
    // 示例输入
    std::vector<int> prices = {7, 1, 5, 3, 6, 4};
    // 调用函数计算最大利润
    int result = maxProfit(prices);
    // 输出最大利润
    std::cout << "Max Profit: " << result << std::endl;
    return 0;
}