得物 一面
ArrayList 和 LinkedList 的区别是什么?
-
实现原理
- ArrayList 底层实现为一个 Object 类型的动态数组,内存连续。
- LinkedList 底层实现为双向链表,基于节点(Node)。
-
时间复杂度
- ArrayList:尾部插入和删除 O(1)、其他位置插入和删除 O(n)、查找 O(1)
- LinkedList:尾部和头部插入和删除 O(1)、其他位置插入和删除 O(n)、查找 O(n)
此外 ArrayList 添加元素时如果需要扩容,则需要复制原数组到更大的数组,操作的时间复杂度为 O(n)
ArrayList 的底层原理是什么?
ArrayList 底层实现是 Object 类型的动态数组实现的,其核心机制包括
- 动态扩容:默认初始容量 10,当 ArrayList 容量不足时,会创建一个新的、更大的数组(默认扩容 1.5 倍),然后将原数组的数据复制到新数组。
newCapacity = oldCapacity + (oldCapacity >> 1)
- 缩容机制:ArrayList 不会自动缩容,需手动调用 trimToSize()插入操作
ArrayList 是线程安全的吗?
ArrayList 不是线程安全的,因为它的所有操作(如 add、remove)都没有同步控制。如果多个线程同时修改 ArrayList,可能会导致数据不一致或抛出 ConcurrentModificationException。
eg
线程A和B同时执行add(): 线程A检查容量足够,准备插入位置index = size 线程B抢先插入数据,导致线程A写入时覆盖数据 最终size的值可能小于实际元素数量
为什么 ArrayList 不是线程安全的,具体来说是哪里不安全
首先来看源码,ArrayList 的 add 操作包含两个关键步骤
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查并扩容
elementData[size++] = e; // 赋值并自增size
return true;
}
在高并发情况下,ArrayList 会暴露出如下问题
-
size++ 非原子操作
- 这个问题基本每次都会发生。因为 size++ 本身不为原子性操作。size 可分为三步:1️⃣ 获取 size 值,2️⃣ 将 size+1,3️⃣ 覆盖掉旧 size
线程 A 和线程 B 拿到一样的 size 值同时完成了覆盖,就会导致实际上只 size++ 了一次,所以肯定和 add 的实际数量不同。
- 这个问题基本每次都会发生。因为 size++ 本身不为原子性操作。size 可分为三步:1️⃣ 获取 size 值,2️⃣ 将 size+1,3️⃣ 覆盖掉旧 size
-
数据覆盖
- 假设 size=0,线程 A 将元素写入 test[0]后但是还未执行 size++,此时 B 线程也写入了 test[0],就会导致线程 A 的值被覆盖。然后 AB 线程先后执行 size++,但实际只写入了一个有效元素,但 size=2
-
值覆盖
- 线程 A 检查发现当前 size 为 9,无需扩容。线程写入 test[9],但未执行 size++。线程 B 进来也一样也发现不需要扩容,然后写入 test[9],导致数据覆盖。且 size 最终增加为 11,但 test[9]之后位置未被填充。
-
数组越界
- 线程 A 在扩容后 size=15,线程 B 未感知到扩容,尝试写入 test[10],导致 ArrayIndexoutofBoundsExceptio
ArrayList 和 LinkedList 的应用场景,什么时候该用哪个?
-
ArrayList
- 高频按索引访问
- 内存敏感(数组空间利用率高)
-
LinkedList
- 频繁头部 / 尾部插入删除(如队列、栈)
- 无需预知数据量大小(动态扩展无扩容开销)。
缓存雪崩、缓存击穿、缓存穿透是什么?对应的解决方案
📄((20250311173048-4wn721h ‘Redis三大件 穿透、雪崩、击穿’))
缓存雪崩
简单来说就是大量缓存在同一时间过期,或者 Redis 宕机,此时大量请求都会涌入数据库,造成数据库压力剧增
解决方法:
- 可以给缓存额外加上一个随机的过期时间,保证数据不会在同一时间过期
- 或者是可以用下面的逻辑过期时间或分布式锁的办法。但是基本上加一个随机过期时间就可以很便捷的解决这个问题。
缓存击穿
简单来说就是热点数据突然过期,导致大量请求涌入数据库
解决方法:
-
分布式锁:控制一个线程去数据库获取资源。保证同一时间只有一个业务更新缓存,未获得锁的请求,要么等待锁释放后重新读取缓存,要么返回空值或默认值。
-
逻辑过期时间:Redis 中热点数据不设置过期时间,转而在应用层面来设置一个过期时间。当应用层过期时,让新来的数据继续访问 Redis 中的旧数据,然后控制一个异步线程去数据库获取数据。
缓存穿透
简单来说就是访问一条不存在的数据,既然数据库中不存在这条数据,那么缓存中固然也不会存在。所以所有攻击就会被穿透到数据库。
解决方法:
- 布隆过滤器:将数据库中存在的元素添加到布隆过滤器中,新来的请求先去过滤器里查找,不存在直接拒绝,存在再进行后续的查找
- 缓存空值:即使查到的值为空,也缓存到 Redis 中(设置一个较短的过期时间)。
- 非法请求限制:在 API 入口处进行初步的非法请求的检查,判断请求参数是否合理、请求字段是否存在等等。
HTTP1.1 怎么对请求做拆包,具体来说怎么拆的?
好的在说拆包之前,可以先看一看粘包问题。
什么是粘包问题呢?由于 TCP 是面向字节流的协议,数据没有明确的消息边界,多个应用层报文可能被合并为一个 TCP 包发送(粘包)。
客户端发送两个 HTTP 请求GET /a和GET /b,TCP 可能合并为一个数据包发送:[GET /aGET /b]
了解了粘包问题,我们就可以来看拆包,HTTP 拆包粘包问题的解决方案之一。
-
固定长度模式
-
原理:通过头部字段
Content-Length
明确制定消息体的字节长度 -
流程
- 客户端发送请求时,在 Header 中生命 Content-Length:1024(假设)
- 服务端读取 Header 时,严格按照 Content-Length 读取后续字节流
- 读取完指定长度后,视为请求结束,后续数据处理下一个请求
-
HTTP/1.1 200 OK Content-Length: 11 Content-Type: text/plain Hello World
-
-
分块传输编码(适用于长度不固定的情况)
-
原理:将消息体分割为多个块,每块包含长度 + 数据,最后以空块结束标识
-
流程
- 客户端在 Header 中声明
kuaiTransfer-Encoding: chunked
-
5\r\n // 第一块长度(十六进制) Hello\r\n // 数据 6\r\n // 第二块长度 World!\r\n // 数据 0\r\n // 结束块 \r\n // 结束符
- 服务端按块长度逐块读取,直到遇到长度为 0 的块。
- 客户端在 Header 中声明
-
三次握手要实现什么目的
-
阻止重复历史连接的初始化
-
eg:客户端先发送了 SYN(seq=90)的报文,然后客户端宕机,然后这个 SYN 报文还被网络阻塞了,服务端并没收到这个请求。接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq=100)。这时旧的 seq=90 的 SYN 先到了服务端
- 一个旧报文比新报文先到了服务端,服务端就会向客户端返回一个 SYN+ACK 报文给客户端,确认号为 91(90+1)
- 客户端收到后,发现自己期望的确认好应该是 101(100+1),而不是 91,于是返回 RST 报文,中断连接。
- 服务端收到 RST 报文后释放连接
- 后续最新的 SYN 到达后,就可以正常建立连接了
-
-
同步双方序列号
若客户端发送 SYN 后崩溃,服务端未收到第三次握手,客户端重启后使用新 ISN 重新发起连接,旧 ISN 对应的报文会被视为过期
-
同步序列号的作用:
- 接收方可以保证顺序和去重
- 接收方可以根据数据包的序列号按序接收
- 可以标识发送出去的数据包中,哪些是已经被对方所接收了的
-
-
避免资源浪费
如果只有两次握手,如果客户端发送的 SYN 在网络阻塞或因其他原因没及时送达,就会导致服务端没有收到请求,所以也就不会返回 ACK 客户端机会重复发送一条 SYN。由于没有三次挥手,所以服务端不知道客户端到底有没有收到自己回复的 ACK,所以服务端每收到一个 SYN 就建立一个连接。这会造成什么情况呢?如果客户端因为网络阻塞发送了多个 SYN,那么最后服务端就会对每个 SYN 都建立起连接,造成不必要的资源浪费
用一个例子来说就像
我和朋友玩游戏,但是我们玩之前需要测试一下麦克风是不是好的,看看能不能听到对方说话。
- 我先说喂喂喂能听到吗?
- 朋友听到了就说:OKOK 能听到。(我知道了我说话对方能听到)
- 我说:OK 能听到赶快上号。(朋友知道了我能听到)
至此三次握手,可靠连接完成。
JWT 令牌和传统令牌有什么区别?
- 无状态:JWT 无需在服务端存储信息,而是全部依赖于 JWT 令牌,减轻了服务端压力
- 安全性能:JWT 会使用密钥对令牌进行签名,确保令牌不会被篡改。只有服务端持有正确密钥才能进行解析。此外因为 JWT 不存储于 Cookie,也结局了 CSRF 跨站请求伪造攻击
- 跨域支持:只需要在请求头携带 JWT 令牌即可
JWT 令牌为什么能解决集群部署,什么是集群部署?
在传统基于会话和 Cookie 的身份验证中,会话信息会存于服务器内存或数据库中。但是在集群中不同服务器之间无法共享会话信息,就会导致用户在不同服务器之间切换需要重新登录,或者引入其他的共享机制(如 Redis),增加了性能开销。
而 JWT 令牌由于具有无状态性,不依赖于服务端存储信息。JWT 令牌自身就携带了身份验证和会话信息。不同的服务器之间只需要有正确的密钥就可以正确的解析出 JWT 令牌,而无需引入额外的共享机制。
JWT 令牌有哪些字段?
有三个字段,
-
头部:用于描述令牌元数据,通常包含两个字段
- 签名算法:如 HS256
- 令牌类型:JWT
-
载荷:用来携带一些额外的信息,比如用户名、用户 ID 等。但需要注意载荷是不加密的,所以不能存放敏感信息。此外过期时间是放在载荷里的注册声明中。
-
签名:对头部、载荷 Base64 编码后,再对编码后的结果和密钥一起进行签名后的结果
JWT 令牌如果泄露了,怎么解决,JWT 是怎么做的
首先可以直接更换服务器密钥,使所有令牌失效
但是这个方法太暴力了,那有其他办法吗?有,但是需要牺牲 JWT 的无状态性
-
黑名单:在 Redis 或其他地方存储需要加入黑名单的 jwt 或者 jti(jwt 唯一标识)
-
刷新令牌:服务端存储长效令牌、给客户端办法短期令牌。当短令牌泄露后,服务端就标记长令牌失效。即使令牌泄露,也能最小的降低损失。
-
去掉无状态:JWT 中存储一个 Key,redis 中键使用 jwt 中的 key,值为之前放在载荷里的信息。每次验证时查询 redis,如果令牌泄露直接让 reids 中的值过期即可。
SHA256 是加密还是签名?加密和签名有什么区别?
SHA256 属于哈希算法(Hash Algorithm),并非直接的加密或签名算法。
SHA256 的作用是将任意长度的字符转为固定长度(256)的唯一哈希值,确保数据完整。其中如果某一位发生微小的改动,也会导致最后的哈希值发生显著的变化。且无法从哈希值反推出原数据。
SHA256 在签名中的作用。
- 生成摘要:对消息计算 SHA256 哈希值。
- 签名:使用私钥对哈希值加密(如 RSA 签名)。
- 验证:接收方用公钥解密签名得到哈希值,重新计算消息哈希并比对
签名 VS 加密?
维度 | 加密 | 签名 |
---|---|---|
目的 | 加密数据,防止被未授权读取 | 确保数据完整性和真实性(防篡改)不保证机密性 |
密钥使用 | 对称加密:单密钥 非对称加密:公钥加密、私钥解密 | 私钥签名,公钥验证 |
数据流向 | 加密后数据可逆(需要密钥解密) | 签名不可逆(仅用于验证) |
应用场景 | 传输敏感数据 | 合同签署、软件发布 |