1. 项目背景与核心需求
华为OD(Online Judge)机考系统是华为技术面试中的重要环节,其中双机位监考模式能够有效防止作弊行为。C卷作为中高级难度题库,常考察候选人对多线程、任务调度等复杂场景的处理能力。本次题目"启动多任务排序"正是典型的生产者-消费者模型变种,要求开发者实现多任务的有序启动机制。
在实际开发中,类似场景广泛存在于:
- 分布式任务调度系统(如Hadoop作业调度)
- 微服务架构中的服务启动依赖管理
- 自动化测试中的用例执行顺序控制
2. 技术方案设计
2.1 题目要求解析
题目通常会给出:
- 一组任务及其依赖关系(如任务B依赖任务A完成)
- 并发执行的工作线程数量限制
- 需要输出符合依赖关系的任务执行序列
示例输入可能为:
code复制任务列表:[A,B,C,D]
依赖关系:B->A, C->A, D->B
并发数:2
2.2 拓扑排序基础
这是典型的DAG(有向无环图)拓扑排序问题。我们需要:
- 构建入度表(每个任务的依赖计数)
- 维护就绪队列(入度为0的任务)
- 使用线程池控制并发
java复制// 入度表示例
Map<String, Integer> inDegree = new HashMap<>();
inDegree.put("A", 0);
inDegree.put("B", 1); // B依赖A
2.3 并发控制方案
Java中推荐使用:
ExecutorService固定大小线程池CountDownLatch实现任务完成通知ReentrantLock保证入度更新的线程安全
java复制ExecutorService executor = Executors.newFixedThreadPool(concurrency);
CountDownLatch latch = new CountDownLatch(1); // 用于任务完成通知
3. 核心实现细节
3.1 数据结构设计
java复制class TaskNode {
String taskName;
List<TaskNode> dependencies;
// 构造函数、getter/setter省略
}
3.2 拓扑排序主流程
java复制public List<String> scheduleTasks(List<TaskNode> tasks, int concurrency) {
// 1. 初始化入度表和邻接表
Map<String, Integer> inDegree = new HashMap<>();
Map<String, List<String>> adjList = new HashMap<>();
// 2. 构建图结构(代码细节见下文)
// 3. 使用PriorityQueue保证任务按特定顺序执行
Queue<String> queue = new PriorityQueue<>();
// 4. 线程池执行任务
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
// ...完整实现约150行代码...
}
3.3 线程安全处理要点
java复制// 使用ReentrantLock保证入度更新原子性
private void updateInDegree(String taskName,
Map<String, Integer> inDegree,
Queue<String> queue,
ReentrantLock lock) {
lock.lock();
try {
int newDegree = inDegree.get(taskName) - 1;
if (newDegree == 0) {
queue.offer(taskName);
}
inDegree.put(taskName, newDegree);
} finally {
lock.unlock();
}
}
4. 完整实现代码
java复制public class MultiTaskScheduler {
private final ExecutorService executor;
private final ReentrantLock lock = new ReentrantLock();
public List<String> schedule(List<TaskNode> tasks, int concurrency) {
// 初始化数据结构
Map<String, Integer> inDegree = new HashMap<>();
Map<String, List<String>> adjList = new HashMap<>();
Queue<String> queue = new LinkedList<>();
// 构建图
for (TaskNode task : tasks) {
String name = task.getTaskName();
inDegree.put(name, task.getDependencies().size());
if (task.getDependencies().isEmpty()) {
queue.offer(name);
}
for (TaskNode dep : task.getDependencies()) {
adjList.computeIfAbsent(dep.getTaskName(), k -> new ArrayList<>())
.add(name);
}
}
// 执行任务
List<String> result = new ArrayList<>();
executor = Executors.newFixedThreadPool(concurrency);
while (!queue.isEmpty()) {
String current = queue.poll();
result.add(current);
executor.execute(() -> {
// 模拟任务执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 更新依赖任务状态
if (adjList.containsKey(current)) {
for (String neighbor : adjList.get(current)) {
lock.lock();
try {
int newDegree = inDegree.get(neighbor) - 1;
inDegree.put(neighbor, newDegree);
if (newDegree == 0) {
queue.offer(neighbor);
}
} finally {
lock.unlock();
}
}
}
});
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.HOURS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return result;
}
}
5. 测试用例设计
5.1 正常场景测试
java复制@Test
public void testBasicDependency() {
TaskNode a = new TaskNode("A");
TaskNode b = new TaskNode("B", Arrays.asList(a));
TaskNode c = new TaskNode("C", Arrays.asList(a));
TaskNode d = new TaskNode("D", Arrays.asList(b));
MultiTaskScheduler scheduler = new MultiTaskScheduler();
List<String> result = scheduler.schedule(Arrays.asList(a,b,c,d), 2);
assertTrue(result.indexOf("A") < result.indexOf("B"));
assertTrue(result.indexOf("A") < result.indexOf("C"));
assertTrue(result.indexOf("B") < result.indexOf("D"));
}
5.2 异常情况测试
java复制@Test(expected = RuntimeException.class)
public void testCircularDependency() {
TaskNode a = new TaskNode("A");
TaskNode b = new TaskNode("B", Arrays.asList(a));
a.getDependencies().add(b); // 形成循环依赖
new MultiTaskScheduler().schedule(Arrays.asList(a,b), 2);
}
6. 性能优化建议
-
并行度优化:
- 根据CPU核心数动态调整线程池大小
- 使用
Runtime.getRuntime().availableProcessors()获取核心数
-
数据结构优化:
- 使用
ConcurrentHashMap替代手动加锁 - 考虑
Phaser替代CountDownLatch实现更灵活的同步
- 使用
-
死锁预防:
java复制// 在拓扑排序前检测环 private void checkCyclicDependency(Map<String, List<String>> adjList) { // 使用DFS或Kahn算法检测环 }
7. 常见问题排查
-
任务卡死:
- 检查是否有未处理的循环依赖
- 确保所有叶节点(无依赖任务)都能被加入队列
-
执行顺序异常:
- 验证入度更新是否线程安全
- 检查邻接表构建是否正确
-
内存溢出:
- 限制最大任务数量(华为OD通常限制在1000个任务内)
- 使用
ThreadPoolExecutor的拒绝策略处理过载
关键提示:在华为OD环境中,系统会自动检测线程创建数量,超出限制会导致判题失败。建议始终使用固定大小的线程池。
8. 华为OD机考特殊要求
-
双机位环境注意:
- 禁止使用
System.exit() - 所有异常必须捕获处理
- 控制台输出必须完全符合题目要求
- 禁止使用
-
代码规范:
- 类名必须为
Main - 使用JDK8特性(华为OD环境通常为Java8)
- 避免使用第三方库
- 类名必须为
-
时间控制:
- 算法时间复杂度应控制在O(N+M)(N为任务数,M为依赖数)
- 使用
System.currentTimeMillis()进行本地耗时测试
9. 扩展思考
-
加权任务调度:
java复制class TaskNode { String name; int weight; // 任务权重 List<TaskNode> dependencies; } -
故障恢复机制:
- 记录任务执行状态
- 实现断点续执行功能
-
分布式扩展:
- 使用ZooKeeper协调多节点任务分配
- 考虑引入消息队列(如Kafka)解耦
在实际面试中,面试官可能会追问:
- 如何设计超时中断机制?
- 如果任务需要重试,如何修改架构?
- 如何实现任务优先级调度?
这些扩展问题都值得在平时练习中深入思考。