字节客户端实习一面

作为传奇耐面王,已经是被字节第三个部门面试了,之前两个后端的都挂了,无奈只能另谋出路,本想直接到测开,但这一旦测试开始,估计后续就无缘后端了,所以再挣扎一下试试客户端,实在不行再去测开。

首先是自我介绍以及对项目的简单问答,下面记录关于八股的内容以及参考答案

1、线程与进程是什么有什么区别?线程比进程更高效的原因是什么?

名称

定义

举例

进程(Process)

操作系统中资源分配的最小单位。每个运行的程序就是一个进程。

打开“微信.exe”就是一个进程

线程(Thread)

操作系统中CPU调度的最小单位。线程是进程内部的“执行流”。

微信的一个线程负责接收消息,另一个线程负责播放语音

区别

定义

操作系统分配资源的基本单位

CPU调度的基本单位

内存空间

每个进程拥有独立的地址空间

同一进程下的线程共享内存空间

通信方式

进程间通信(IPC)复杂,如管道、共享内存、消息队列

线程间通信简单,可直接访问共享变量

创建开销

创建、销毁进程开销大

创建、销毁线程开销小

切换成本

进程切换涉及内存映射切换、上下文切换等

线程切换只涉及寄存器、栈等少量数据

稳定性

一个进程崩溃不影响其他进程

一个线程崩溃可能导致整个进程崩溃

资源共享

不共享堆、数据段、文件描述符等

共享堆、静态变量、打开的文件句柄等

线程为什么比进程更高效?

线程高效的原因主要有三点👇:

1️⃣ 创建成本低

  • 创建一个进程时,系统要为它分配独立的内存空间、页表、文件描述符等;
  • 而线程只需在当前进程空间内分配一个栈区和寄存器上下文即可。

举例:进程的创建 ≈ “开一家新餐厅”;

线程的创建 ≈ “招聘一个新服务员”。

2️⃣ 上下文切换成本低

  • 进程切换 → 切换整个内存映射(页表)、寄存器状态、内核栈;
  • 线程切换 → 只需切换寄存器、栈指针,仍在同一地址空间内。

因此线程切换通常比进程切换快 10~100倍

3️⃣ 通信更高效

  • 进程间通信(IPC)需要操作系统内核参与;
  • 线程间通信直接读写共享内存变量即可。

所以线程之间协作更紧密,数据交换几乎无延迟。

2、什么是线程上下文切换?

线程上下文切换是多线程编程中的一个概念,它直接影响程序的性能和效率。接下来我会详细讲述线程上下文切换的定义、发生时机、过程和影响。

首先讲一下什么是线程上下文切换,它是指当 CPU 从一个线程切换到另一个线程时,操作系统需要保存当前线程的执行状态,并加载下一个线程的执行状态,以便它们能够正确地继续运行。执行状态主要包括:寄存器状态、程序计数器(PC)、栈信息、线程的优先级等。

接下来讲一下发生时机,通常有四种情况会发生线程上下文切换。

第一种是时间片耗尽,操作系统为每个线程分配了一个时间片,当线程的时间片用完后,操作系统会强制切换到其他线程,这是为了保证多个线程能够公平地共享 CPU 资源。

第二种是线程主动让出 CPU,当线程调用了某些方法,如 Thread.sleep()、Object.wait() 或 LockSupport.park()等,会使线程主动让出 CPU,导致上下文切换。

第三种是调用了阻塞类型的系统中断,比如:线程执行 I/O 操作时,由于 I/O 操作通常需要等待外部资源,线程会被挂起,会触发上下文切换。

第四种是被终止或结束运行

然后再讲一下线程上下文切换的过程,分为四步。

第一步是保存当前线程的上下文,将当前线程的寄存器状态、程序计数器、栈信息等保存到内存中。

第二步是根据线程调度算法,如:时间片轮转、优先级调度等,选择下一个要运行的线程。

第三步是加载下一个线程的上下文,从内存中恢复所选线程的寄存器状态、程序计数器和栈信息。

第四步是 CPU 开始执行被加载的线程的代码

最后讲一下线程上下文切换所带来的影响。线程上下文切换虽然能够实现多任务并发执行,但它也会带来 CPU 时间消耗、缓存失效以及资源竞争等问题。为了减少线程上下文切换带来的性能损失,可以采取减少线程数量、使用无锁数据结构等方式进行优化。

3、乐观锁和悲观锁分别是什么以及其底层实现

🧩 一、基本概念对比

类型

思路

应用场景

悲观锁

(Pessimistic Lock)

认为“别人肯定会来抢资源”,所以访问前先上锁再操作。

高并发写多场景(银行转账)

乐观锁

(Optimistic Lock)

认为“别人一般不会冲突”,所以访问时不加锁,提交时再检查有没有冲突。

读多写少场景(电商库存扣减)

⚙️ 二、悲观锁的原理与实现

1️⃣ 核心思想

悲观锁在操作数据前,假定会发生并发冲突,所以会直接锁定资源

➡️ 即:一个线程获得锁后,其他线程只能等待该线程释放。

2️⃣ 数据库层实现

MySQL(InnoDB引擎) 中常见有两种:

✅ (1)共享锁(S锁)

允许多个事务同时读取一条记录,但不允许修改。

SELECT * FROM product WHERE id = 1 LOCK IN SHARE MODE;

✅ (2)排他锁(X锁)

不允许其他事务读或写。

SELECT * FROM product WHERE id = 1 FOR UPDATE;

注意:此锁会在事务提交或回滚后释放。

🚨 注意事项:

  • 必须在事务中使用(BEGIN ... COMMIT);
  • 被锁定的行会阻塞其他试图访问它的事务;
  • 如果没有索引,会锁全表(性能灾难)。

3️⃣ Java 层实现(synchronized / ReentrantLock)

在 Java 中,synchronizedReentrantLock 都是典型的悲观锁实现:

synchronized (this) {
    // 临界区代码
}

➡️ 当一个线程进入同步块,其他线程会阻塞等待。

这种方式性能较低,但能确保严格的线程安全。

💡 三、乐观锁的原理与实现

1️⃣ 核心思想

它是一种基于“无锁”思想的并发控制机制。它假设多线程操作之间很少发生冲突,因此在读取数据时不会加锁,而是通过某种机制(如版本号或时间戳)来检测数据是否被其他线程修改过。如果检测到数据未被修改,则提交更新;如果检测到数据已被修改,则根据策略进行处理(如重试或抛出异常)。

接下来说一下乐观锁的实现方式,乐观锁的实现通常依赖于以下两种机制:

一种是版本号机制:为数据添加一个版本号字段,每次更新时递增版本号,并在更新时验证版本号是否匹配。

另一种是CAS 操作:使用比较并交换(Compare-And-Swap)指令,直接在硬件层面实现无锁操作。CAS 操作包含内存位置(V)、预期值(A)和新值(B)这三个参数。只有当内存位置的值等于预期值时,才会将内存位置的值更新为新值。

2️⃣ 数据库实现(最典型方式)

假设有个商品表:

id

name

stock

version

1

手机

10

1

执行逻辑:

1️⃣ 读数据

SELECT stock, version FROM product WHERE id = 1;

2️⃣ 扣减库存时携带版本号

UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;

3️⃣ 判断是否更新成功(affected_rows == 1

  • 成功:表示没人改过,可以提交;
  • 失败:表示版本号被改(有人先一步修改了),需重新读取再尝试。

✅ 这就是 CAS机制(Compare And Swap) 的思想。

3️⃣ Java层实现(CAS操作)

java.util.concurrent.atomic 包中,比如 AtomicInteger

AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(expect: 0, update: 1);

底层通过 CPU 指令 cmpxchg 实现原子比较与交换,无需加锁,性能极高。

🔬 四、底层实现机制总结

实现层面

悲观锁

乐观锁

数据库层

行锁(S/X锁)、表锁

版本号、时间戳、CAS

Java层

synchronized、ReentrantLock

AtomicXXX(CAS)

性能特征

性能低、冲突安全

性能高、冲突需重试

适用场景

写多、冲突频繁

读多写少、冲突少

底层机制

内核锁/数据库锁机制

CPU硬件原语(CAS)

4、讲解一下锁的升级是什么

1.锁的状态与升级过程

(1)锁的状态

在Java中,synchronized关键字和ReentrantLock等锁机制都涉及锁的状态管理。锁的状态通常可以分为以下几种:

无锁状态(Unlocked):当一个对象或资源没有被任何线程持有锁时,它处于无锁状态。此时,多个线程可以自由访问该资源。

偏向锁(Biased Locking):偏向锁是一种优化机制,用于减少无竞争情况下的同步开销。当一个线程第一次获取锁时,JVM会将锁标记为偏向该线程,并记录线程ID。如果后续该线程再次尝试获取锁,无需进行额外的同步操作,直接判断线程ID是否匹配即可。偏向锁适用于只有一个线程访问同步块的场景。

轻量级锁(Lightweight Locking):当有第二个线程尝试获取已经被偏向的锁时,偏向锁会升级为轻量级锁。轻量级锁通过CAS(Compare-And-Swap)操作来尝试获取锁。如果CAS操作成功,则线程获取锁;如果失败,则进入自旋等待状态,尝试多次获取锁。

重量级锁(Heavyweight Locking):当多个线程竞争锁且自旋等待无法快速获取锁时,轻量级锁会升级为重量级锁。重量级锁会将未获取锁的线程挂起(进入阻塞状态),并由操作系统调度。这种方式会带来较大的性能开销,因为线程的挂起和唤醒需要上下文切换。

(2)锁的升级过程

锁的升级过程是一个从低开销到高开销的逐步演化过程,目的是在不同竞争程度下选择最优的锁实现。以下是锁升级的具体流程:

初始状态:无锁,对象刚创建时,没有任何线程竞争锁,处于无锁状态。

偏向锁,第一个线程尝试获取锁时,JVM会将锁标记为偏向锁,并记录线程ID。后续该线程再次尝试获取锁时,只需检查线程ID是否匹配,无需额外操作。

轻量级锁,当第二个线程尝试获取锁时,偏向锁失效,升级为轻量级锁。轻量级锁通过CAS操作尝试获取锁。如果CAS操作失败,线程会进入自旋状态,反复尝试获取锁。

重量级锁,如果自旋一定次数后仍然无法获取锁,或者系统检测到锁竞争激烈,轻量级锁会升级为重量级锁。重量级锁会将未获取锁的线程挂起,避免CPU资源浪费。

(3)锁升级的意义

锁升级的核心目的是在不同的竞争场景下平衡性能和资源消耗:

偏向锁:适合单线程频繁访问的场景,减少同步开销。

轻量级锁:适合少量线程竞争的场景,利用CAS和自旋提高效率。

重量级锁:适合高竞争场景,避免线程长时间占用CPU资源。

(4)锁降级

需要注意的是,锁的升级是单向的,即从无锁 → 偏向锁 → 轻量级锁 → 重量级锁。一旦锁升级为重量级锁,就不会再降级为轻量级锁或偏向锁。

5、读写锁是什么?

🧩 一、为什么需要读写锁?

先看一个问题:

假设我们使用普通的 synchronizedReentrantLock

ReentrantLock lock = new ReentrantLock();

public void read() {
    lock.lock();
    try {
        // 读取共享数据
    } finally {
        lock.unlock();
    }
}

public void write() {
    lock.lock();
    try {
        // 修改共享数据
    } finally {
        lock.unlock();
    }
}

👉 缺点:

即使是多个读操作,也不能并发执行(因为同一把锁)。

但实际上,“读”操作不会修改共享资源,可以同时进行

于是就有了:

ReentrantReadWriteLock(可重入读写锁)

⚙️ 二、读写锁的基本原理

ReentrantReadWriteLock 内部维护了 两种锁

锁类型

作用

特点

读锁(ReadLock)

允许多个线程同时获取

共享锁(shared)

写锁(WriteLock)

同一时刻只能有一个线程获取

独占锁(exclusive)

也就是说:

  • 多个线程 同时读取数据(不会冲突);
  • 但有线程要写时,必须等所有读操作结束才能写;
  • 写操作期间,其他线程 既不能读也不能写。

🧩 三、底层原理

ReentrantReadWriteLock 的实现基于 AbstractQueuedSynchronizer (AQS)

  • 写锁使用 独占模式(exclusive mode);
  • 读锁使用 共享模式(shared mode);
  • 内部用一个 32 位整数的高低位来同时记录 读锁数量 和 写锁状态。

简要逻辑:

State(32位)
 ├── 高16位:读锁计数
 └── 低16位:写锁计数

CAS + AQS 队列 来确保原子性与公平性。

🧮 四、ReentrantReadWriteLock 的常见特性

特性

说明

可重入

同一线程可多次获得读锁或写锁

公平/非公平模式

默认非公平锁(性能更高)

升级

写锁 → 读锁:允许(会立即释放写锁后再加读锁)

降级

读锁 → 写锁:不允许(容易死锁)

条件变量

仅写锁支持 newCondition()

⚖️ 五、适用场景

场景

适合的锁

读多写少

✅ ReadWriteLock(性能更高)

写多读少

普通独占锁(ReentrantLock)

临界区很小

synchronized

典型应用:

  • 缓存读取场景:多线程频繁读取缓存数据,偶尔更新(例如配置中心、热点数据读取)
  • 游戏状态查询(读操作频繁)
  • 本地数据快照等

6、介绍OSI的7层网络模型,讲解网络层、传输层、应用层常见的协议

🌐 一、OSI 七层网络模型概述

从上到下,OSI 模型共分为 七层

层级

名称

功能

常见协议

7

应用层(Application)

面向用户,提供应用服务接口

HTTP、FTP、SMTP、DNS

6

表示层(Presentation)

数据格式转换、加密解密、压缩

SSL/TLS、JPEG、MPEG

5

会话层(Session)

建立、管理、终止会话

RPC、NetBIOS

4

传输层(Transport)

提供端到端通信,保证数据可靠或高效传输

TCP、UDP

3

网络层(Network)

负责路径选择与逻辑寻址(IP 地址)

IP、ICMP、ARP

2

数据链路层(Data Link)

提供点到点的数据帧传输

MAC、PPP、Ethernet

1

物理层(Physical)

负责比特流的传输

光纤、电缆、网卡、电压标准

🧭 二、重点讲解三层

1️⃣ 网络层(Network Layer)

核心功能:负责“路由选择”和“寻址”——即找到数据从源主机到目标主机的最佳路径。

关键点:

  • 逻辑地址(IP地址)的使用
  • 路由与转发(Router 设备在此层工作)
  • 拆分和重组数据包(分片)

常见协议:

  • IP(Internet Protocol):定义 IP 地址,负责数据包传递。IPv4、IPv6
  • ICMP(Internet Control Message Protocol):用于诊断(如 ping 命令)
  • ARP(Address Resolution Protocol):将 IP 地址解析为 MAC 地址
  • RIP、OSPF、BGP:动态路由协议

2️⃣ 传输层(Transport Layer)

核心功能:实现端到端的通信,为上层提供可靠(或不可靠)的数据传输。

关键点:

  • 端口号(区分不同应用)
  • 传输的可靠性控制(确认 ACK、重传、滑动窗口)
  • 流量控制与拥塞控制

常见协议:

  • TCP(Transmission Control Protocol)面向连接(三次握手、四次挥手)可靠传输(确认、重传、顺序保证)有流量控制、拥塞控制
  • UDP(User Datagram Protocol)无连接,不保证可靠传输轻量级,实时性高(用于视频流、DNS、语音)

3️⃣ 应用层(Application Layer)

核心功能:直接为用户或应用程序提供服务,是人机交互的接口层。

常见协议与用途:

协议名称

功能

实例

HTTP / HTTPS

网页访问

浏览器访问网站

FTP

文件传输

上传下载文件

SMTP / POP3 / IMAP

邮件传输

邮件收发

DNS

域名解析

将域名转为 IP 地址

DHCP

动态主机配置

自动分配 IP 地址

SNMP

网络管理

管理设备状态

7、详细讲解TCP协议,其建立机制、释放机制、可靠传输、流量控制、拥塞控制

一、TCP 是什么(一句话)

TCP 是面向连接、可靠、字节流(stream)的传输层协议,提供端到端的可靠数据传输、顺序交付、流量控制与拥塞控制。

二、连接建立:三次握手(3-way handshake)

目的是双方同步初始序列号(ISN),建立双向可靠通道。

步骤(A 为客户端,B 为服务器):

  1. A → B:SYN = 1, Seq = x(客户端发起,告诉服务器:我要建立连接,序号 x)
  2. B → A:SYN = 1, ACK = 1, Seq = y, Ack = x+1(服务器应答并回报自己的序号 y,同时确认客户端)
  3. A → B:ACK = 1, Seq = x+1, Ack = y+1(客户端确认,连接建立)

比喻:像借物登记

  • 客户端先打招呼并说“我的编号 x”(SYN)
  • 服务器回“我也有编号 y,并收到你的 x”(SYN+ACK)
  • 客户端再回一句“好,我收到 y 了”(ACK)

目的:避免已过期的连接请求被误用(利用序列号),并确保双方都准备好了接收数据。

三、连接释放:四次挥手(4-way close)

TCP 的半关闭(half-close)特性导致释放为四次挥手:

假设 A 主动关闭:

  1. A → B:FIN, Seq = u(A 表示自己没有更多数据要发)
  2. B → A:ACK, Ack = u+1(B 确认收到 A 的 FIN)(现在 A → B 数据流为关闭,但 B → A 仍可能有数据)
  3. B → A:FIN, Seq = v(B 也准备关闭)
  4. A → B:ACK, Ack = v+1(A 确认,连接完全关闭)

另外有 RST(reset)用于异常立即断开。TIME_WAIT 状态(主动关闭的一方通常进入)用于确保最后的 ACK 能被对端接收以及让重复分组在网络中消失(通常保持 2×MSL,MSL 是最大报文寿命)。

四、可靠传输机制

TCP 用一系列手段保证可靠、按序交付。

1. 序列号与确认(Seq / Ack)

  • 每个字节都有序列号,报文段带有 Seq 和长度,确认通过 Ack(累计确认)完成:Ack = 下一个期望字节序号。

2. 滑动窗口(Sliding Window)

  • 发送方维护 [SendBase, NextSeq) 的窗口;接收方维护可接收窗口 rwnd(receiver window)告诉发送方还能接收多少字节。
  • 发送方可以在未收到 ACK 时发送窗口内的数据(流水线)。

3. 重传机制(Retransmission)

  • 基本方式:设置 RTO(重传超时),超时未确认就重传。
  • 快速重传:当发送方收到 3 个重复 ACK(表示某一段丢失但后续到达)时,立即重传相应段,不需等 RTO。
  • 选择性确认(SACK,可选扩展):接收方告知哪些区间已收到,发送方只重传丢失的区间,提高效率。

4. RTT 与 RTO 估算

  • 使用样本 RTT(仅对未重传数据有效),用 Jacobson/Kare n 算法估算 RTO:EstimatedRTT 和 DevRTT,RTO = EstRTT + 4*DevRTT。避免重传风暴。

5. 数据校验(Checksum)

  • TCP 报文头 + 数据有校验和,确保位错误检测。

五、流量控制(Flow Control)

目标:保护接收方不被发送方淹没(端到端接收能力控制)。

  • 通过接收窗口 rwnd 实现:接收方在接收 ACK 中回告发送方自己可用的缓冲大小(字节数)。发送方不得超过 min(cwnd, rwnd) 的窗口发送。
  • rwnd = 0 会让发送方暂停发送,但必须处理“零窗口探测”以检测何时恢复。

注意:流量控制针对的是端点接收能力,不关心网络拥塞。

六、拥塞控制(Congestion Control)

目标:避免或减轻网络拥塞(路由器/链路负载过重),提高整体网络吞吐量与公平性。与流量控制不同,拥塞控制是网络级别的。

现代 TCP 拥塞控制的经典组成(以 TCP Reno/NewReno 为基础):

关键变量

  • cwnd(拥塞窗口)—— 发送方认为网络可承受的字节数(与 rwnd 共同决定发送窗口)
  • ssthresh(慢启动阈值)

算法阶段

  1. 慢启动(Slow Start)初始 cwnd 通常为 1~10 MSS(MSS = 最大报文段大小)。每收到一个 ACK,cwnd += MSS(指数增长:每 RTT 翻倍),直到达到 ssthresh 或发生丢包。
  2. 拥塞避免(Congestion Avoidance)达到 ssthresh 后,cwnd 线性增长:每 RTT 增加 ~1 MSS(典型实现 cwnd += MSS*MSS/cwnd 即加 1 MSS/RTT)。
  3. 检测丢包 & 反应

超时(RTO)发生:认为严重拥塞,ssthresh = cwnd/2,cwnd 重置为 1 MSS,进入慢启动(保守)。

Three Dup ACK(快速重传):触发快速重传并进入快速恢复(Fast Recovery):ssthresh = cwnd/2cwnd = ssthresh + 3*MSS(Reno)进入拥塞避免后的调整。NewReno 对快速恢复的行为更友好,SACK 进一步改进。

经典变体

  • TCP Tahoe:丢包后把 cwnd 设为 1,ssthresh = cwnd/2(较保守)
  • TCP Reno / NewReno:引入快速重传/快速恢复,性能更好
  • TCP Cubic(Linux 默认现代算法):以立方函数控制 cwnd,在高带宽-延迟网络中表现好

8、HTTP 协议和 HTTPS分别是什么,如何实现的

一、HTTP 是什么

HTTP(HyperText Transfer Protocol) —— 超文本传输协议。

是一个 应用层协议,用于在 客户端(浏览器)与服务器 之间传输数据(如 HTML、图片、JSON 等)。

  • 工作在 OSI 第七层(应用层)
  • 默认端口:80
  • 无状态(Stateless)
  • 基于 TCP(传输层) 连接传输数据

举例

客户端:GET /index.html HTTP/1.1
服务端:返回网页内容(200 OK + HTML)

HTTP 本身不加密,也不保证数据完整性或身份认证。所有内容(包括用户名、密码、Cookie)都以明文形式传输。

二、HTTPS 是什么

HTTPS(HTTP Secure / HTTP over SSL/TLS)

= HTTP + SSL/TLS(安全层)

它在 HTTP 与 TCP 之间加了一层 加密层(SSL 或 TLS)

工作流程:

  1. 浏览器先与服务器建立 TLS 安全连接
  2. 双方协商加密算法、生成密钥
  3. 使用该密钥进行加密通信(HTTP 内容被加密)

默认端口:443

三、HTTP 与 HTTPS 的区别(面试高频对比)

全称

HyperText Transfer Protocol

HyperText Transfer Protocol Secure

端口

80

443

加密

明文传输

SSL/TLS 加密传输

安全性

无认证、易被窃听篡改

支持身份认证、加密和防篡改

证书

不需要证书

需要数字证书(CA 颁发)

性能

开销较小

握手阶段稍慢(建立安全连接)

一句话总结:

HTTP 快但不安全;HTTPS 稍慢但安全(通过加密 + 证书实现保密、认证和完整性)。

四、HTTPS 的核心:TLS 握手与加密机制

1️⃣ 建立安全连接的总体流程(TLS 握手)

以最常见的 TLS 1.2 为例:

客户端                服务器
   | ----- ClientHello -----> |  客户端发送支持的加密算法、随机数1
   | <---- ServerHello ------ |  服务器选定算法、返回随机数2
   | <--- Certificate ------- |  服务器发送数字证书(包含公钥)
   | ----- ClientKeyExchange ->|  客户端用公钥加密自己的随机数3
   | ----- ChangeCipherSpec ->|  通知服务器后续加密通信
   | <---- ChangeCipherSpec --|  双方用协商好的密钥对称加密传输数据

2️⃣ TLS 加密的三大核心机制

非对称加密

握手阶段(密钥交换)

RSA、ECDHE

对称加密

实际数据传输

AES、ChaCha20

摘要算法

数据完整性验证

SHA256、MD5(已过时)

  • 非对称加密:客户端使用服务器公钥加密“会话密钥”
  • 对称加密:握手后使用“会话密钥”进行快速加密通信
  • 摘要算法(MAC):确保数据未被篡改

3️⃣ 数字证书与 CA 认证机制

  • CA(Certificate Authority):权威机构,颁发服务器身份认证证书
  • 证书内容:公钥、域名、颁发者、有效期、签名算法
  • 验证方式:客户端验证 CA 签名是否合法、域名是否匹配、证书是否过期

这样可以防止中间人攻击(MITM)冒充网站。

9、对称加密和非对称加密区别是什么?CA认证机制是什么?

一、对称加密 vs 非对称加密的区别

密钥数量

1 把密钥(加解密相同)

2 把密钥(公钥、私钥)

速度

快(加解密效率高)

慢(加解密计算量大)

安全性

需要安全地分发密钥,否则可能泄露

公钥公开也没问题,只需保护私钥

典型算法

AES、DES、ChaCha20

RSA、ECC(椭圆曲线)、DSA

👉 总结

  • 对称加密适合 大数据传输(速度快)。
  • 非对称加密适合 密钥交换和身份验证(安全性高)。

二、HTTPS 中两者如何配合使用

在 HTTPS 建立连接时,浏览器和服务器会经历一个 握手(Handshake)阶段

  1. 客户端发起连接请求(带上自己支持的加密算法列表等信息)
  2. 服务器返回证书(包含公钥、公钥签名等,由 CA 签发)
  3. 客户端验证证书合法性(验证颁发机构的签名、域名一致性、有效期等)
  4. 生成随机对称密钥(Session Key)客户端使用服务器的公钥(RSA 或 ECC)加密这个 Session Key服务器用自己的私钥解密,得到相同的 Session Key
  5. 后续通信用对称加密(如 AES)进行加密传输

🔹 这样做的好处:

  • 非对称加密仅用于一次性的“密钥交换”(安全)
  • 对称加密用于后续数据传输(高效)

三、CA 认证机制是什么?

CA(Certificate Authority,证书颁发机构)是整个 HTTPS 信任体系的根基。它的作用是:

证明服务器(或客户端)的身份真实可信。

1️⃣ 证书内容

一个标准的数字证书(如 .crt 文件)包含:

  • 公钥(Public Key)
  • 域名信息(比如 www.example.com)
  • 签发机构(CA 名字)
  • 有效期
  • CA 的数字签名

2️⃣ 验证过程

当浏览器访问一个网站时:

  1. 网站返回自己的证书(包含公钥和 CA 签名);
  2. 浏览器用 内置的 CA 公钥 验证这个签名;
  3. 验证通过 → 信任该网站的公钥;
  4. 后续用该公钥加密生成对称密钥,建立安全通信。

3️⃣ 为什么信任 CA?

操作系统和浏览器内置了全球权威 CA 的公钥(例如 DigiCert、GlobalSign、Let's Encrypt)。

——所以,只要证书是它们签发的,就能被信任。

10、事务的四大特性是什么?

我之前八股有记录写过,第3点就是https://www.nowcoder.com/discuss/798590234995720192?sourceSSR=users

我就简单写了

A(原子性)C(持续性)I(隔离性)D(持久性)

11、幻读是什么?为什么会出现

这个我也之前写过,在第7条,https://www.nowcoder.com/discuss/802546933209264128?sourceSSR=users

幻读就是两次读取前后的结果条目不同,出现的原因在于两次读取中间有别的事务插入了新数据

12、索引有哪些类型

常见的索引类型:

  • 主键索引(聚簇索引):数据按主键存储在 B+Tree 的叶子节点中。
  • 普通索引(非聚簇索引):叶子节点存储的是主键值,通过主键再去表里查数据(二次回表)。
  • 唯一索引:不允许重复值。
  • 组合索引:多个字段联合组成的索引。

作者:凝尘木匠链接:https://www.nowcoder.com/discuss/798590234995720192?sourceSSR=users

13、面向对象三大特性,多态在实际开发中如何体现?

一、面向对象三大特性

1️⃣ 封装(Encapsulation)

把数据(属性)和操作数据的方法绑定在一起,并隐藏内部实现细节,对外只暴露必要的接口。

示例:

public class User {
    private String name;  // 属性私有化
    public void setName(String name) { // 提供对外接口
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

👉 优点:隐藏实现细节,提高安全性和复用性。

2️⃣ 继承(Inheritance)

子类继承父类的属性和方法,实现代码复用。

示例:

class Animal {
    public void eat() {
        System.out.println("动物在吃东西");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("狗在叫");
    }
}

好处: 避免重复代码,增强可维护性。

3️⃣ 多态(Polymorphism)⭐

同一个行为在不同对象上表现出不同的形态。

多态有两种形式:

  • 编译时多态(方法重载 Overload)
  • 运行时多态(方法重写 Override)

二、多态在开发中的体现(重点)

✅ 实际体现一:面向接口编程

例如一个支付系统:

interface PayService {
    void pay(double amount);
}

class AliPay implements PayService {
    public void pay(double amount) {
        System.out.println("使用支付宝支付:" + amount);
    }
}

class WechatPay implements PayService {
    public void pay(double amount) {
        System.out.println("使用微信支付:" + amount);
    }
}

class PayController {
    private PayService payService;
    public PayController(PayService payService) {
        this.payService = payService;
    }
    public void doPay(double amount) {
        payService.pay(amount);
    }
}

public class Main {
    public static void main(String[] args) {
        PayController c1 = new PayController(new AliPay());
        c1.doPay(100);

        PayController c2 = new PayController(new WechatPay());
        c2.doPay(200);
    }
}

👉 程序并不关心“是哪种支付方式”,

只要实现了 PayService 接口即可。

这就是运行时多态:同一方法调用,表现不同实现。

✅ 实际体现二:Spring Bean 注入

在 Spring 框架中,常见的依赖注入(DI)机制其实就是多态的应用:

@Service
public class SmsMessageService implements MessageService {
    public void send(String msg) { System.out.println("短信发送:" + msg); }
}

@Service
public class EmailMessageService implements MessageService {
    public void send(String msg) { System.out.println("邮件发送:" + msg); }
}

@Service
public class MessageController {
    private final MessageService messageService;

    @Autowired
    public MessageController(MessageService messageService) {
        this.messageService = messageService;
    }

    public void sendMessage(String msg) {
        messageService.send(msg);
    }
}

Spring 运行时根据配置或注解,动态注入具体实现类,

控制层永远面向接口编程,不依赖具体实现。

14、判断两个对象相等,为什么不能使用==,而是用equals()?如果两个对象hashcode是相等的,那equals()也是相等的吗?

🧩 一、为什么不能用“==”判断对象相等?

因为 == 比较的是两个引用是否指向同一块内存地址,而不是“内容”是否一样。

✅ 举个例子:

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);       // false
System.out.println(s1.equals(s2));  // true

🔹 分析:

  • ==:比较两个引用是否指向同一块堆内存。→ s1 和 s2 是 new 出来的两个不同对象,地址不同。所以结果是 false。
  • equals():默认比较“值是否相等”,而 String 类重写了 equals() 方法,比较的是内容(字符序列)。所以返回 true。

👉 因此:

== 比较的是对象地址(引用)

equals() 比较的是对象内容(逻辑相等性)

✅ 再举个反例(包装类陷阱):

Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true(缓存机制)
System.out.println(a.equals(b)); // true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false(超出缓存范围)
System.out.println(c.equals(d)); // true

🔹 因为 Java 对 -128 ~ 127 的整数会缓存(IntegerCache),

所以 ab 实际指向同一个对象。

200 超出了缓存范围,所以是两个不同的对象地址。

🧠 二、那 hashCode 和 equals 又是什么关系?

Java 对这两个方法有一条非常重要的“契约”:

如果两个对象通过 equals() 相等,那么它们的 hashCode() 必须相等。

但反之,不成立 —— hashCode 相等,equals 不一定相等。

✅ 原因解释:

hashCode() 是用来支持 哈希表存储(如 HashMap、HashSet) 的。

当我们往 HashMap 中放入一个对象时,流程是这样的:

  1. 先调用对象的 hashCode() → 确定放在哪个桶(bucket);
  2. 如果桶里已有对象,就用 equals() 判断是否为同一个键;
  3. 若 equals() 返回 true → 覆盖旧值;
  4. 否则挂到该桶的链表(或红黑树)上。

✅ 举例:

class Person {
    String name;
    int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 重写equals只比较name
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person p = (Person) o;
        return name.equals(p.name);
    }

    // hashCode返回固定值(错误示范)
    @Override
    public int hashCode() {
        return 1;
    }
}

Person p1 = new Person("Alice", 20);
Person p2 = new Person("Alice", 30);

System.out.println(p1.equals(p2));   // true
System.out.println(p1.hashCode() == p2.hashCode()); // true

✅ 没问题,符合规范。

但是如果:

p1.hashCode() == p2.hashCode() // true
p1.equals(p2) // false

这种情况是允许的,叫 哈希冲突(hash collision)

比如 HashMap 里不同的 key 落到同一个桶。

📘 三、总结一句话口诀:

比较方式

含义

示例

==

比较地址是否相同

new String("a") != new String("a")

equals()

比较内容是否相同(逻辑相等)

new String("a").equals(new String("a"))

hashCode()

计算对象哈希值,用于定位哈希桶

HashMap / HashSet

15、哈希冲突是什么?

🌱 一、哈希表的工作原理

哈希表存储数据时,会通过 哈希函数(Hash Function) 将键(Key)转换为一个整数索引值(hash值),再通过这个索引值在数组中定位存储位置。

例如:

index = hash(key) % table.length

这样就能快速定位到存放的位置,实现接近 O(1) 的查找、插入效率。

⚠️ 二、什么是哈希冲突

由于:

  • 哈希函数输出的范围是有限的(比如数组长度100),
  • 而输入的键可能无限多(比如各种字符串、对象),

所以不同的键可能被映射到同一个索引位置

这种情况就叫做 哈希冲突

📘 例子:

hash("Jack") % 10 == 3
hash("John") % 10 == 3

即使 "Jack" 和 "John" 不同,它们也被映射到同一个槽位(index=3)——这就是哈希冲突。

🧩 三、哈希冲突的常见解决方法

  1. 拉链法(Chaining)每个数组槽位不只存放一个元素,而是一个链表或红黑树。发生冲突时,将新元素追加到该链表中。Java 的 HashMap 在 JDK 1.8 之后就是这样实现的,当链表长度 > 8 时,会转换为红黑树。
  2. 开放地址法(Open Addressing)当冲突发生时,寻找下一个空槽位放入。常见策略:线性探测(+1, +2, …)二次探测(+1², +2², …)双重哈希(使用第二个哈希函数计算偏移量)
  3. 再哈希法(Rehashing)当装载因子(元素数/表长度)超过一定比例(如0.75),自动扩容并重新计算哈希位置。

💡 四、在 Java 中的体现

在 Java 的 HashMap 中:

  • 元素通过 hashCode() 计算哈希值。
  • 若索引相同,则调用 equals() 判断是否为同一键。
  • 若不是同一键,则在该位置形成链表或树结构存储。

16、讲解JVM内存模型

常见的八股,掠过

17、内存回收算法有哪些?

垃圾回收(Garbage Collection,简称 GC)是 Java 虚拟机(JVM)中自动管理内存的重要机制,它通过一系列算法来识别和回收不再使用的对象,从而释放堆内存。接下来我会详细讲述常见的四种垃圾回收算法及其工作原理。

第一个是标记-清除算法(Mark-Sweep),它是最基础的垃圾回收算法,主要分为两个阶段,一个是标记阶段,从根对象(GC Roots)开始,递归遍历所有可达对象,并标记为“存活”; 另一个是清除阶段,遍历整个堆内存,回收未被标记的对象所占用的空间。

此算法主要存在两个问题,一个是内存碎片化,回收后的内存可能会产生大量不连续的碎片,导致大对象无法分配内存;另一个是效率较低,需要两次遍历堆内存,耗时较长。

第二个是复制算法(Copying),它通过将内存划分为两块(From 和 To),每次只使用其中一块,解决了标记-清除算法的内存碎片化问题,主要分为两个阶段,一个是复制阶段,当一块内存用完时,将存活的对象复制到另一块内存中,并按顺序排列;另一个是清理阶段,直接清空原来的内存块,无需额外的标记或清除操作。

此算法的优点是效率高且不会产生内存碎片,但缺点是需要双倍的内存空间。

第三个是标记-整理算法(Mark-Compact),它是对标记-清除算法的改进,它在标记阶段完成后,会将所有存活对象向一端移动,从而避免内存碎片化。主要分为两个阶段,一个是标记阶段,与标记-清除算法相同,标记所有存活对象;另一个是整理阶段,将存活对象移动到内存的一端,清理边界外的内存。

此算法适合老年代(Old Generation),因为老年代中的对象存活率较高,复制成本较大。

第四个是分代收集算法(Generational Collection),它是目前主流 JVM 的垃圾回收策略,它基于对象的生命周期将堆内存划分为新生代(Young Generation)和老年代(Old Generation)。

对于新生代,大多数对象朝生夕灭,采用复制算法进行垃圾回收。新生代进一步划分为 Eden 区和两个 Survivor 区(From 和 To);对于老年代,存活时间较长的对象存储在此,采用标记-清除或标记-整理算法进行垃圾回收。

这种算法结合了不同算法的优点,针对不同代的特点选择合适的回收策略,从而提升整体性能。

18、如果创建一个新对象,有可能直接放入老年代吗?什么条件下会直接放入?

🧩 一、正常情况下的对象分配

在 Java 中,对象一般通过 new 创建,默认分配在 堆内存的 Eden 区(属于新生代)

JVM 内部对象分配流程如下:

  1. 分配线程私有缓存(TLAB);
  2. 如果 TLAB 空间足够,则直接在 TLAB 内分配;
  3. 否则在 Eden 区申请;
  4. 如果 Eden 空间不足,触发 Minor GC;
  5. GC 过程中,存活对象可能晋升到老年代。

🧠 二、但有三种情况会“直接进入老年代”

✅ 1️⃣ 对象太大(大对象直接分配到老年代)

如果一个对象非常大,例如一个很大的数组、图片数据缓存、视频帧缓冲等,

JVM 可能直接分配到老年代,以避免频繁的年轻代 GC 移动。

对应参数是:

-XX:PretenureSizeThreshold=大小(单位字节)

例如:

-XX:PretenureSizeThreshold=10M

表示:如果对象大于 10MB,则直接进入老年代,而不是分配在 Eden 区。

⚠️ 注意:

  • 该参数只在使用 Serial 和 ParNew GC 时有效。
  • 对于 G1、ZGC 等现代收集器,这个参数会被忽略(它们自己有分配逻辑)。

✅ 2️⃣ 长期存活的对象(年龄足够会晋升)

JVM 对每个对象维护一个“年龄计数”,每经历一次 Minor GC,

如果对象还活着,它的年龄就 +1。

当对象的年龄超过某个阈值(默认 15 岁)时,就会晋升到老年代

参数:

-XX:MaxTenuringThreshold

例如:

-XX:MaxTenuringThreshold=10

表示对象在新生代中经过 10 次 Minor GC 仍未回收,就会晋升到老年代。

✅ 3️⃣ 动态年龄判定机制(空间分配担保)

这是一个 JVM 的自适应策略

当 Survivor 区中相同年龄的对象大小之和 超过了 Survivor 区的一半

那么年龄大于或等于该年龄的所有对象,都会直接晋升到老年代

举个例子:

Survivor 区总大小 = 10MB

年龄为 5 的对象占了 6MB (>10/2=5MB)

那么年龄 ≥5 的对象全部进入老年代。

这种机制用于防止 Survivor 区爆满,影响性能。

✅ 4️⃣ 老年代空间分配担保失败(直接晋升)

当进行 Minor GC 时,JVM 会判断老年代是否有足够的空间接收晋升对象。

如果判断发现老年代空间不足以容纳所有可能晋升的对象,

那么这次 GC 会直接把部分对象放入老年代,甚至可能触发 Full GC

💡 三、总结

情况

是否直接进入老年代

触发条件

大对象分配

✅ 是

-XX:PretenureSizeThreshold

长寿命对象

✅ 是

-XX:MaxTenuringThreshold 超过阈值

动态年龄判断

✅ 是

某年龄段对象总和 > Survivor 一半

老年代担保失败

✅ 是

Minor GC 预估晋升对象过多

正常新对象

❌ 否

默认分配到 Eden 区

19、接下来就是针对项目进行设问场景题

总的来说是八股盛宴,我是一半都没答好,磕磕巴巴,也没后文了

全部评论
九✌都找不到吗
点赞 回复 分享
发布于 今天 10:54 湖北
客户端比测开还坑点测开相对还是好跳
点赞 回复 分享
发布于 今天 10:33 日本

相关推荐

天门一键开:她的意思是问你有没有论文吧
点赞 评论 收藏
分享
算法冲刺中:kpi面加一,面完完全没动静,感谢信都没有
点赞 评论 收藏
分享
评论
点赞
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务