字节客户端实习一面
作为传奇耐面王,已经是被字节第三个部门面试了,之前两个后端的都挂了,无奈只能另谋出路,本想直接到测开,但这一旦测试开始,估计后续就无缘后端了,所以再挣扎一下试试客户端,实在不行再去测开。
首先是自我介绍以及对项目的简单问答,下面记录关于八股的内容以及参考答案
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 中,synchronized、ReentrantLock 都是典型的悲观锁实现:
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、读写锁是什么?
🧩 一、为什么需要读写锁?
先看一个问题:
假设我们使用普通的 synchronized 或 ReentrantLock:
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 的常见特性
特性 | 说明 |
可重入 | 同一线程可多次获得读锁或写锁 |
公平/非公平模式 | 默认非公平锁(性能更高) |
升级 | 写锁 → 读锁:允许(会立即释放写锁后再加读锁) |
降级 | 读锁 → 写锁:不允许(容易死锁) |
条件变量 | 仅写锁支持 |
⚖️ 五、适用场景
场景 | 适合的锁 |
读多写少 | ✅ 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 为服务器):
- A → B:SYN = 1, Seq = x(客户端发起,告诉服务器:我要建立连接,序号 x)
- B → A:SYN = 1, ACK = 1, Seq = y, Ack = x+1(服务器应答并回报自己的序号 y,同时确认客户端)
- 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 主动关闭:
- A → B:FIN, Seq = u(A 表示自己没有更多数据要发)
- B → A:ACK, Ack = u+1(B 确认收到 A 的 FIN)(现在 A → B 数据流为关闭,但 B → A 仍可能有数据)
- B → A:FIN, Seq = v(B 也准备关闭)
- 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(慢启动阈值)
算法阶段
- 慢启动(Slow Start)初始 cwnd 通常为 1~10 MSS(MSS = 最大报文段大小)。每收到一个 ACK,cwnd += MSS(指数增长:每 RTT 翻倍),直到达到 ssthresh 或发生丢包。
- 拥塞避免(Congestion Avoidance)达到 ssthresh 后,cwnd 线性增长:每 RTT 增加 ~1 MSS(典型实现 cwnd += MSS*MSS/cwnd 即加 1 MSS/RTT)。
- 检测丢包 & 反应
超时(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)。
工作流程:
- 浏览器先与服务器建立 TLS 安全连接
- 双方协商加密算法、生成密钥
- 使用该密钥进行加密通信(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)阶段:
- 客户端发起连接请求(带上自己支持的加密算法列表等信息)
- 服务器返回证书(包含公钥、公钥签名等,由 CA 签发)
- 客户端验证证书合法性(验证颁发机构的签名、域名一致性、有效期等)
- 生成随机对称密钥(Session Key)客户端使用服务器的公钥(RSA 或 ECC)加密这个 Session Key服务器用自己的私钥解密,得到相同的 Session Key
- 后续通信用对称加密(如 AES)进行加密传输
🔹 这样做的好处:
- 非对称加密仅用于一次性的“密钥交换”(安全)
- 对称加密用于后续数据传输(高效)
三、CA 认证机制是什么?
CA(Certificate Authority,证书颁发机构)是整个 HTTPS 信任体系的根基。它的作用是:
证明服务器(或客户端)的身份真实可信。
1️⃣ 证书内容
一个标准的数字证书(如 .crt 文件)包含:
- 公钥(Public Key)
- 域名信息(比如 www.example.com)
- 签发机构(CA 名字)
- 有效期
- CA 的数字签名
2️⃣ 验证过程
当浏览器访问一个网站时:
- 网站返回自己的证书(包含公钥和 CA 签名);
- 浏览器用 内置的 CA 公钥 验证这个签名;
- 验证通过 → 信任该网站的公钥;
- 后续用该公钥加密生成对称密钥,建立安全通信。
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),
所以 a、b 实际指向同一个对象。
而 200 超出了缓存范围,所以是两个不同的对象地址。
🧠 二、那 hashCode 和 equals 又是什么关系?
Java 对这两个方法有一条非常重要的“契约”:
如果两个对象通过 equals() 相等,那么它们的 hashCode() 必须相等。
但反之,不成立 —— hashCode 相等,equals 不一定相等。
✅ 原因解释:
hashCode() 是用来支持 哈希表存储(如 HashMap、HashSet) 的。
当我们往 HashMap 中放入一个对象时,流程是这样的:
- 先调用对象的 hashCode() → 确定放在哪个桶(bucket);
- 如果桶里已有对象,就用 equals() 判断是否为同一个键;
- 若 equals() 返回 true → 覆盖旧值;
- 否则挂到该桶的链表(或红黑树)上。
✅ 举例:
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 落到同一个桶。
📘 三、总结一句话口诀:
比较方式 | 含义 | 示例 |
| 比较地址是否相同 |
|
| 比较内容是否相同(逻辑相等) |
|
| 计算对象哈希值,用于定位哈希桶 |
|
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)——这就是哈希冲突。
🧩 三、哈希冲突的常见解决方法
- 拉链法(Chaining)每个数组槽位不只存放一个元素,而是一个链表或红黑树。发生冲突时,将新元素追加到该链表中。Java 的 HashMap 在 JDK 1.8 之后就是这样实现的,当链表长度 > 8 时,会转换为红黑树。
- 开放地址法(Open Addressing)当冲突发生时,寻找下一个空槽位放入。常见策略:线性探测(+1, +2, …)二次探测(+1², +2², …)双重哈希(使用第二个哈希函数计算偏移量)
- 再哈希法(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 内部对象分配流程如下:
- 分配线程私有缓存(TLAB);
- 如果 TLAB 空间足够,则直接在 TLAB 内分配;
- 否则在 Eden 区申请;
- 如果 Eden 空间不足,触发 Minor GC;
- 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。
💡 三、总结
情况 | 是否直接进入老年代 | 触发条件 |
大对象分配 | ✅ 是 |
|
长寿命对象 | ✅ 是 |
|
动态年龄判断 | ✅ 是 | 某年龄段对象总和 > Survivor 一半 |
老年代担保失败 | ✅ 是 | Minor GC 预估晋升对象过多 |
正常新对象 | ❌ 否 | 默认分配到 Eden 区 |
vivo公司福利 363人发布