贪心算法应用:最短作业优先(SJF)调度问题详解
Java中的贪心算法应用:最短作业优先(SJF)调度问题详解
一、贪心算法基础概念
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法策略。
贪心算法的特点:
- 局部最优选择:在每一步都做出当前看起来最佳的选择
- 不可回退:一旦做出选择,就不会再改变
- 高效性:通常比其他全局最优算法更高效
- 不保证全局最优:在某些问题中可能无法得到全局最优解
贪心算法的适用条件:
- 贪心选择性质:局部最优选择能导致全局最优解
- 最优子结构:问题的最优解包含其子问题的最优解
二、最短作业优先(SJF)问题概述
最短作业优先(Shortest Job First, SJF)是一种CPU调度算法,它选择下一个具有最短执行时间的进程/作业来执行。SJF可以是:
- 非抢占式:一旦CPU分配给进程,进程会一直运行直到完成
- 抢占式:也称为最短剩余时间优先(SRTF),新进程到达时,如果其执行时间比当前进程剩余时间短,则抢占CPU
SJF算法的优点:
- 最小化平均等待时间
- 最大化系统吞吐量
SJF算法的缺点:
- 可能导致长作业饥饿
- 需要预先知道或估计作业的执行时间
- 实现比FCFS(先来先服务)更复杂
三、SJF问题的贪心算法分析
在SJF调度中,贪心算法的体现是:每次总是选择剩余作业中最短的一个来执行。这种局部最优选择(每次选最短作业)能够导致全局最优(最小化平均等待时间)。
数学证明:
设有n个作业,执行时间分别为t₁, t₂, …, tₙ,按执行时间排序为t₁ ≤ t₂ ≤ … ≤ tₙ。
作业i的等待时间Wᵢ = Σ(tⱼ) for j=1 to i-1
总等待时间 = ΣWᵢ = Σ(Σtⱼ) for j=1 to i-1, i=1 to n
要最小化总等待时间,应该让短作业先执行,因为短作业在前会减少后面所有作业的等待时间。
四、非抢占式SJF的Java实现
1. 作业/进程类定义
class Process {int pid; // 进程IDint arrivalTime; // 到达时间int burstTime; // 执行时间public Process(int pid, int arrivalTime, int burstTime) {this.pid = pid;this.arrivalTime = arrivalTime;this.burstTime = burstTime;}
}
2. 非抢占式SJF算法实现
import java.util.*;public class NonPreemptiveSJF {// 计算等待时间和周转时间static void findAvgTime(Process[] processes) {int n = processes.length;int[] waitingTime = new int[n];int[] turnaroundTime = new int[n];// 按到达时间排序Arrays.sort(processes, Comparator.comparingInt(p -> p.arrivalTime));// 创建优先队列,按执行时间排序PriorityQueue<Process> pq = new PriorityQueue<>(Comparator.comparingInt(p -> p.burstTime));int currentTime = 0;int index = 0;int completed = 0;while (completed != n) {// 将所有到达时间<=currentTime的进程加入队列while (index < n && processes[index].arrivalTime <= currentTime) {pq.add(processes[index]);index++;}if (pq.isEmpty()) {currentTime = processes[index].arrivalTime;continue;}// 获取执行时间最短的进程Process current = pq.poll();// 计算等待时间waitingTime[current.pid] = currentTime - current.arrivalTime;// 计算周转时间turnaroundTime[current.pid] = waitingTime[current.pid] + current.burstTime;// 更新当前时间currentTime += current.burstTime;completed++;}// 计算平均等待时间和周转时间float totalWaitingTime = 0, totalTurnaroundTime = 0;for (int i = 0; i < n; i++) {totalWaitingTime += waitingTime[i];totalTurnaroundTime += turnaroundTime[i];}System.out.println("平均等待时间 = " + (totalWaitingTime / n));System.out.println("平均周转时间 = " + (totalTurnaroundTime / n));}public static void main(String[] args) {Process[] processes = {new Process(0, 0, 6),new Process(1, 2, 8),new Process(2, 4, 7),new Process(3, 5, 3)};findAvgTime(processes);}
}
3. 代码解析
- 排序阶段:首先按到达时间排序所有进程
- 优先队列:使用最小堆(优先队列)来快速获取当前可执行的最短作业
- 时间推进:
- 将已到达的进程加入队列
- 如果队列为空,时间跳到下一个进程到达时间
- 取出最短作业执行
- 计算等待时间和周转时间
- 统计结果:计算并输出平均等待时间和周转时间
五、抢占式SJF(SRTF)的Java实现
抢占式SJF,即最短剩余时间优先(Shortest Remaining Time First, SRTF),实现更为复杂。
1. 抢占式SJF算法实现
import java.util.*;public class PreemptiveSJF {static class Process {int pid;int arrivalTime;int burstTime;int remainingTime;int completionTime;int waitingTime;int turnaroundTime;public Process(int pid, int arrivalTime, int burstTime) {this.pid = pid;this.arrivalTime = arrivalTime;this.burstTime = burstTime;this.remainingTime = burstTime;}}static void findAvgTime(Process[] processes) {int n = processes.length;int currentTime = 0;int completed = 0;// 按到达时间排序Arrays.sort(processes, Comparator.comparingInt(p -> p.arrivalTime));// 优先队列,按剩余时间排序PriorityQueue<Process> pq = new PriorityQueue<>(Comparator.comparingInt(p -> p.remainingTime));int index = 0;Process current = null;while (completed != n) {// 将到达的进程加入队列while (index < n && processes[index].arrivalTime <= currentTime) {pq.add(processes[index]);index++;}// 如果有更高优先级的进程(更短剩余时间)if (current != null && !pq.isEmpty() && pq.peek().remainingTime < current.remainingTime) {pq.add(current);current = pq.poll();}// 如果没有正在执行的进程,从队列取一个if (current == null && !pq.isEmpty()) {current = pq.poll();}// 时间推进currentTime++;if (current != null) {current.remainingTime--;// 如果进程完成if (current.remainingTime == 0) {current.completionTime = currentTime;current.turnaroundTime = current.completionTime - current.arrivalTime;current.waitingTime = current.turnaroundTime - current.burstTime;completed++;current = null;}}}// 计算平均时间float totalWaitingTime = 0, totalTurnaroundTime = 0;for (Process p : processes) {totalWaitingTime += p.waitingTime;totalTurnaroundTime += p.turnaroundTime;}System.out.println("平均等待时间 = " + (totalWaitingTime / n));System.out.println("平均周转时间 = " + (totalTurnaroundTime / n));}public static void main(String[] args) {Process[] processes = {new Process(0, 0, 8),new Process(1, 1, 4),new Process(2, 2, 9),new Process(3, 3, 5)};findAvgTime(processes);}
}
2. 代码解析
- 进程类扩展:增加了剩余时间、完成时间等字段
- 时间推进:以单位时间(1)推进
- 抢占逻辑:
- 每次时间推进前检查是否有更高优先级(更短剩余时间)的进程
- 如果有,当前进程放回队列,执行新进程
- 进程完成:当剩余时间为0时,计算各项时间指标
- 统计结果:计算并输出平均等待时间和周转时间
六、SJF算法的性能分析
时间复杂度分析
-
非抢占式SJF:
- 排序:O(n log n)
- 优先队列操作:每次插入和删除O(log n),共n次 → O(n log n)
- 总时间复杂度:O(n log n)
-
抢占式SJF(SRTF):
- 排序:O(n log n)
- 每个时间单位可能有一次队列操作:最坏情况O(T log n),T为总执行时间
- 实际应用中,T可能很大,因此SRTF开销较大
空间复杂度分析
两种实现都使用了优先队列,最坏情况下需要存储所有进程:
- 空间复杂度:O(n)
七、SJF算法的变种与优化
1. 预测执行时间
实际系统中作业的执行时间往往未知,可采用以下方法预测:
- 指数平均法:τₙ₊₁ = αtₙ + (1-α)τₙ
- τₙ₊₁:下一次预测值
- tₙ:实际执行时间
- τₙ:上一次预测值
- α:权重参数(0≤α≤1)
2. 多级反馈队列(MFQ)
结合SJF和轮转调度(RR)的优点:
- 新作业进入最高优先级队列(短作业优先)
- 如果作业运行时间过长,降级到下一队列
- 较低优先级队列采用更长时间片的RR
- 防止长作业饥饿
3. 带优先级的SJF
为每个作业分配优先级,结合优先级和剩余时间进行调度:
调度优先级 = f(剩余时间, 静态优先级)
八、实际应用场景
- 批处理系统:已知作业长度的计算任务
- 嵌入式系统:实时性要求高的场景
- 操作系统调度:结合其他算法使用
- 网络包调度:短包优先传输
九、完整示例与测试
测试案例1:非抢占式SJF
public class TestNonPreemptiveSJF {public static void main(String[] args) {Process[] processes = {new Process(0, 0, 6),new Process(1, 2, 2),new Process(2, 4, 1),new Process(3, 5, 3),new Process(4, 6, 4)};NonPreemptiveSJF.findAvgTime(processes);}
}
输出分析:
执行顺序: P0(0-6), P1(6-8), P2(8-9), P3(9-12), P4(12-16)
等待时间: P0(0), P1(4), P2(4), P3(4), P4(6)
平均等待时间 = 3.6
测试案例2:抢占式SJF(SRTF)
public class TestPreemptiveSJF {public static void main(String[] args) {PreemptiveSJF.Process[] processes = {new PreemptiveSJF.Process(0, 0, 8),new PreemptiveSJF.Process(1, 1, 4),new PreemptiveSJF.Process(2, 2, 9),new PreemptiveSJF.Process(3, 3, 5)};PreemptiveSJF.findAvgTime(processes);}
}
输出分析:
时间线:
0-1: P0
1-2: P1(到达), P0剩余7 > P1剩余4 → 执行P1
2-3: P2到达, P1剩余3最小 → 继续P1
3-4: P3到达, P1剩余2最小 → 继续P1
4-5: P1完成, 剩余: P0(7), P2(9), P3(5) → 执行P3
5-6: P3剩余4最小 → 继续P3
...
平均等待时间比非抢占式更优
十、与其他调度算法的比较
算法 | 平均等待时间 | 响应时间 | 吞吐量 | 公平性 | 实现复杂度 |
---|---|---|---|---|---|
FCFS | 高 | 高(对短作业) | 中 | 高 | 低 |
SJF(非抢占) | 最低 | 中 | 高 | 低(长作业饥饿) | 中 |
SRTF(抢占) | 最低 | 最低 | 高 | 低 | 高 |
RR | 中 | 低 | 中 | 高 | 中 |
优先级 | 取决于优先级 | 取决于优先级 | 中 | 低 | 中 |
十一、常见问题与解决方案
问题1:长作业饥饿
解决方案:
- 老化(Aging):随着等待时间增加,逐步提高作业优先级
- 多级反馈队列:长作业降级但仍有执行机会
- 设置最大等待时间阈值
问题2:执行时间未知
解决方案:
- 使用历史数据预测
- 采用混合调度策略
- 用户提供时间估计(如批处理系统)
问题3:上下文切换开销
解决方案:
- 为抢占设置最小时间阈值
- 采用非抢占式SJF
- 优化上下文切换机制
十二、Java实现中的优化技巧
-
使用更高效的数据结构:
// 使用更快的排序方法 Arrays.sort(processes, (p1, p2) -> p1.arrivalTime - p2.arrivalTime);// 使用自定义比较器避免装箱 PriorityQueue<Process> pq = new PriorityQueue<>((p1, p2) -> p1.burstTime - p2.burstTime );
-
减少对象创建:重用进程对象而非频繁创建
-
并行处理:对于大规模作业,可考虑并行处理到达的作业
-
时间模拟优化:在抢占式实现中,可以跳到下一个关键时间点而非单位时间推进
十三、总结
最短作业优先(SJF)调度算法是贪心算法在操作系统调度中的经典应用,通过每次选择最短作业执行的贪心策略,能够有效减少平均等待时间。Java实现中利用优先队列(最小堆)可以高效地管理就绪队列,快速获取最短作业。
非抢占式SJF实现相对简单,适合批处理系统;抢占式SJF(SRTF)能获得更好的性能但实现复杂,上下文切换开销大。实际系统中常采用SJF的变种或与其他算法结合使用,以平衡性能和公平性。
理解SJF算法的贪心本质和Java实现细节,不仅有助于掌握调度算法,也是学习贪心算法应用的良好范例。