C++ 单例面试题(耗时 init + 失败返回空 + 线程安全)+力扣128. 最长连续序列
LeetCode 128. 最长连续序列 C++ 解法解析
题目核心
给定一个未排序的整数数组,找出数字连续的最长序列的长度,要求 O(n) 时间复杂度。
核心思路:哈希集合 + 只从起点开始计数
1. 去重存入哈希集合
将所有数字存入 unordered_set,去重的同时实现 O(1) 查找。
2. 只从连续段的"起点"开始统计
关键判断:只有当 num - 1 不在集合中时,num 才是一个连续段的起点。
- ✅ 如果是起点 → 开始循环找
num+1、num+2……统计长度 - ❌ 如果不是起点 → 跳过,因为它必被从更小的起点开始的统计覆盖
3. 更新全局最大长度
复杂度分析
| 项目 | 复杂度 |
|---|---|
| 时间复杂度 | O(n)(每个元素最多被访问两次) |
| 空间复杂度 | O(n)(哈希集合存储) |
C++ 代码实现
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
// 1. 存入哈希集合去重
unordered_set<int> set;
for (int num : nums) {
set.insert(num);
}
int maxLen = 0;
// 2. 遍历集合中的每个数
for (int num : set) {
// 关键:只有是连续段起点时才统计
if (!set.count(num - 1)) {
int cur = num;
int len = 1;
// 向后找连续的数字
while (set.count(cur + 1)) {
cur++;
len++;
}
maxLen = max(maxLen, len);
}
}
return maxLen;
}
};
C++ 单例面试题(耗时 init + 失败返回空 + 线程安全)
使用说明:以下内容以“面试官期望听到的答案逻辑”为出发点,进行了高度结构化梳理。省略了代码细节和现场对话口吻,只保留严密的推导过程和核心知识点,非常适合快速复习和背诵。
一、 题目考点定位(破题)
面试官主要想考察以下三个层面的能力:
- 对象生命周期管理:如何将“构造”与“耗时初始化”解耦,以处理初始化失败的情况。
- 错误处理机制:如何打破传统单例“永远返回有效对象”的惯例,向外部暴露“创建失败”的语义(返回空)。
- 高并发下的稳健性:不仅保证首次创建时的线程安全,还要解决“初始化失败后,后续线程如何重试”这一深水区问题。
二、 基础方案的选择与淘汰(第一层)
核心论点:教科书上的标准写法无法满足本题约束,必须推翻重来。
- C++11 Meyers Singleton(函数内静态局部变量)为什么不适用?
- 缺陷 1:构造函数无法返回错误码,无法向外部传达“初始化失败”的信号。
- 缺陷 2:接口返回的是引用,无法返回空指针(nullptr)来表示对象不可用。
结论:必须将“对象内存分配”与“业务逻辑初始化(init)”拆分成两个独立的步骤,并将 getInstance 的返回类型修改为指针语义。
三、 进阶方案的陷阱(第二层)
核心论点:std::call_once 看起来完美,实则暗藏致命的生产环境风险。
- 使用思路:利用
std::once_flag保证耗时init()在多线程下只执行一次。 - 致命缺陷(面试必杀技):
once_flag一旦被触发,无论内部的init()执行结果是成功还是失败,该标志位都会被置为“已调用”状态。- 后果:如果首次执行因为磁盘 IO 错误或网络超时而失败,后续所有线程在调用时都会直接跳过初始化逻辑,导致该单例永远返回空指针。服务在故障恢复后无法自愈,必须重启进程。
结论:call_once 适用于“初始化绝不会失败”的场景,在本业务背景下必须放弃。
四、 终极解决方案(第三层,核心重点)
核心论点:引入“原子状态机”配合“双重检查锁(DCLP)”思想,实现失败可重试。
1. 状态定义
维护一个全局的、线程可见的状态枚举,包含以下四种状态:
- 未初始化:初始状态,对象尚未创建。
- 初始化中:标记当前有线程正在执行耗时的
init,防止其他线程重复进入。 - 初始化成功:对象已就绪,可正常提供服务。
- 初始化失败:本次尝试失败,允许后续请求重新触发初始化流程。
2. 获取实例的运行流程(双路径设计)
-
快速路径(无锁读取):
- 条件:当状态为“初始化成功”时。
- 行为:所有线程直接读取已发布的对象指针,不涉及互斥锁竞争,保证极高并发性能。
-
慢速路径(加锁与状态切换):
- 触发条件:状态为“未初始化”或“初始化失败”。
- 执行逻辑:
- 线程尝试获取互斥锁。
- 获取锁后,再次检查状态(防止等待期间已被其他线程初始化成功)。
- 若确认为“未初始化”或“失败”,先将状态置为“初始化中”,然后执行真正的耗时
init()。 - 分支处理:
init()返回 true → 状态更新为“成功”,发布对象指针。init()返回 false → 状态回退为“失败”,释放锁,外部获取到空指针。
- 其他处于等待或稍后重试的线程,检测到“失败”状态时会重新进入慢速路径,从而实现了故障恢复后的自动重试。
五、 深度防护措施(第四层,加分项)
核心论点:除了逻辑正确,还需保证底层内存模型的绝对安全。
1. 内存序与指令重排(可见性)
- 潜在风险:编译器和 CPU 可能会对赋值操作进行重排序。如果对象指针的写入发生在
init逻辑完成之前,其他线程可能拿到一个未初始化完全的对象。 - 解决方案:
- 状态变量的读取必须使用获取语义,确保后续的读操作不会提前执行。
- 状态变量的写入必须使用释放语义,确保之前的初始化写入全部落盘。
- 通过这种配对关系,在不加额外重量级屏障的前提下,保证跨线程的可见性。
2. 异常安全
- 耗时
init()内部绝对不能向外抛出原生异常。 - 策略:在
init()内部用try-catch包裹所有可能异常的操作,统一转换为布尔值(true/false)返回。保证状态机始终运行在可控的布尔逻辑下。
六、 总结陈词(价值升华)
- 权衡取舍:虽然没有使用最简单的
call_once,但手动维护状态机换来了业务的弹性恢复能力。 - 性能保证:一旦初始化成功,后续读取路径完全无锁,不影响高频调用场景。
- 设计原则:该方案完美契合“面向接口编程”和“单一职责”原则,将“对象创建”、“业务加载”、“状态流转”清晰解耦,是应对生产环境不稳定因素的成熟工程手段。

浙公网安备 33010602011771号