Java控制台实现操作系统课设:调度、内存与阻塞完整模拟

简介:这是一份基于Java控制台实现的操作系统课程设计完整资料,面向计算机专业学生及需要完成操作系统课设的开发者。内容将作业调度、内存管理、进程调度与进程阻塞唤醒有机整合:采用先来先服务算法管理后备队列,时间片轮转调度内存中的就绪/运行进程,首次适应算法分配内存并在进程结束时回收合并空闲分区,同时限定并发进程数最多为5个,完整呈现了一个可运行的多模块模拟系统。资源包共8个文件,含4个Java源程序、1份PDF报告、1份DOC文档、1个Markdown说明及1个License文件,压缩包大小约1.28MB。其中Java源码为核心实现,PDF/DOC为课设报告,MD便于快速了解结构。已有259人浏览学习。读者可获得可直接运行的Java代码、配套课程设计报告与说明文档,既能对照理解FCFS/RR/FF等经典算法的代码落地,也可作为自己编写操作系统模拟器的参考模板。

1. 为什么一个Java控制台程序,能装下操作系统的三门核心课

操作系统课设年年有,能用纯Java控制台程序把作业调度、内存管理、进程调度、进程阻塞全部串起来跑的,其实比想象中少。多数人的做法是拆成两三个独立小实验:算一遍SJF平均周转时间就算交差,内存管理单独写个分区分配器,进程调度再画几张状态图——每个模块都“对”,但串不到一起。而这门基于Java实现的(控制台)操作系统课设,要的是把这一整套放进同一个黑匣子程序里:作业从进入后备队列开始,经过调度成为进程,在内存中参与分配与回收,在CPU上被按策略调度,遇到资源不足或I/O请求时进入阻塞,之后又被唤醒。它的价值不在算法有多高级,而在状态流转能不能闭环、每步结果能不能在控制台里被看见。

适合谁?一种是正在做操作系统课设、需要“能交差且能答辩”的学生;另一种是想把零散知识点拼成完整系统的自学开发者。老实说,用Java做这事比C++多一层抽象,少了指针级的内存模拟爽感,但换来了清晰的类结构和可读性——你写的是“操作系统模拟器”而不是真正的操作系统。本文按我实际搭过的一版方案讲:四个模块的类怎么设计、调度参数怎么定、死锁怎么避、控制台输出怎么排,以及测试用例怎么设计会让答辩时更有说服力。

2. 课设的整体架构:从单个进程到四大模块的Java类设计

2.1 四个模块的功能边界,以及为什么必须用状态机串起来

做课设最常见的翻车原因是边界模糊:作业调度和进程调度看起来都是“排队”,内存管理和进程调度又都涉及“能不能运行”,最后代码揉成一团。第一件事就是用类定义切开职责。

模块对应类职责核心数据
作业调度JobScheduler从外存后备队列选择作业创建进程JobQueue、JobControlBlock
进程调度ProcessScheduler从就绪队列选进程分配CPUReadyQueue、PCB、TimeSlice
内存管理MemoryManager分区分配、回收、地址转换PartitionList、MemoryBlock
进程阻塞BlockManager等待I/O或资源时挂起与唤醒BlockQueue、Semaphore

这四个类不直接互相调用,而是通过一个OSKernel主类作为中枢,由它持有PCB集合、内存分区列表和各个队列。PCB(进程控制块)是贯穿全局的“档案袋”,每次状态切换都改它的字段:进程ID、状态、所需内存、已运行时间、阻塞原因、在外存/内存中的位置。状态机是整个课设的脊柱,没有它,调度就是一堆数组在乱转。

状态划分建议用经典五态:新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)、退出(Exit),外加一个作业层面的“后备(Hold)”状态。作业调度做的是 Hold → Ready(创建PCB并分配内存),进程调度做的是 Ready → Running → Ready 或 Blocked,内存回收发生在 Blocked → Ready 或 Running → Exit 时。下面的状态迁移图用代码中的枚举表达最为稳妥。

public enum ProcessState { HOLD, // 后备:作业在外存,尚未创建进程 READY, // 就绪:已分配内存,等待CPU RUNNING, // 运行:占用CPU BLOCKED, // 阻塞:等待I/O或资源 EXIT // 退出:运行完毕,等待回收 }

定义顺序上有讲究:BLOCKED必须排在RUNNING之后,因为在进程调度的循环里,你常见到state == RUNNING和state == BLOCKED的判断,相邻枚举便于逻辑阅读。这个枚举类放在model包下,所有调度器都引用它,避免字符串比较出错。

2.2 PCB与内存分区的数据结构:用Java对象建模OS内核对象

PCB是这个课设最重要的类,没有之一。字段的取舍直接决定后续内存管理和调度好不好写:

public class PCB { private int pid; private ProcessState state; private int requiredMemory; // 作业所需内存(KB) private int allocatedBase; // 分配到的内存起始地址 private int allocatedSize; // 实际分配的内存大小 private int cpuTimeUsed; // 已占用CPU时间 private int cpuTimeTotal; // 需要的总CPU时间 private int priority; // 优先级,数字越小优先级越高 private String blockReason; // 阻塞原因,如"I/O"或"等待内存" // 如使用动态优先级,可加 agingCounter(老化计数器) }

我的建议是:requiredMemory、cpuTimeTotal在作业创建时设定,allocatedBase和allocatedSize由内存管理器赋值,blockReason在进入阻塞前设置。这五个字段缺一不可,优先级排课设检查表里最常被问的问题就是“你的进程被阻塞后,它的内存还在吗”——答案当然是还在,因为 PCB 里记录了地址,内存管理器只有在进程退出时才回收。

内存管理用动态分区方式,数据结构是ArrayList<MemoryBlock>:

public class MemoryBlock { private int base; // 起始地址 private int size; // 分区大小(KB) private boolean free; // true表示空闲 private PCB owner; // 占用者,空闲时为null }

这就是标记为经典的分区分配表。每次加载新作业时,遍历MemoryBlock列表找一块free == true && size >= requiredMemory的分区;分配成功则把原块分裂成“占用块 + 剩余空闲块”。回收时把状态改回free并尝试与相邻空闲块合并——不合并会积累大量无法使用的小碎片,后面内存管理的坑里会细讲。

2.3 一版最小可跑框架:主循环与定时器

控制台程序的核心是一个“心跳循环”:每个时间单位(tick)代表一个时间片,程序在每轮 tick 里执行一次完整的“作业调度 → 进程调度 → 阻塞检查 → 输出状态”流程。这个机制让非实时操作系统的模拟变得可观测。

// OSKernel.java 核心调度循环 public void start() { int totalTicks = 300; // 总运行轮数,默认300 for (int tick = 0; tick < totalTicks; tick++) { jobScheduler.scanNewJobs(); // 检查是否有新的作业到达 jobScheduler.dispatch(); // 将满足条件的作业创建为进程 memoryManager.allocatePending(); // 给新建进程分配内存 processScheduler.schedule(); // 从就绪队列选一个进程运行 blockManager.checkBlocked(); // 检查阻塞进程是否可唤醒 printSnapshot(tick); // 输出当前各队列与内存状态 sleep(100); // 每轮间隔100ms,便于观察 } }

totalTicks控制模拟总时长,默认300轮;每轮sleep(100)是为了让你在控制台能看清状态变化,不是真等0.1秒就够。真正答辩时建议把这个间隔调到50ms,否则300轮要等30秒,审阅老师等不住。allocatePending()单独抽出来是因为作业调度只是“选中”作业,内存是否分配成功还要看分区状态——把这两步拆开,后续处理“作业等待内存”这个阻塞场景时会清晰很多。

3. 进程调度与阻塞机制的Java实现:从优先数算法到状态翻转

3.1 作业调度选哪种算法:先来先服务还是短作业优先

两个算法都做再对比才是课设的正确姿态。FCFS(先来先服务)按到达时间排序,代码三五行就能写完;SJF(短作业优先)按预计运行时间排序,能显著降低平均周转时间,但需要预先知道每个作业估计的运行时间——这在模拟环境里不违和,因为作业属性本身由测试数据给定。

实际实现时,我建议做SJF带“到达时间门限”:每轮扫描时,只取出arrivalTime <= currentTick的作业,在其中选cpuTimeTotal最小的送入就绪队列。直接对全部作业排序是典型的错误,那样会把尚未到达的“未来作业”提前调度,平均周转时间计算结果看着漂亮但逻辑站不住。

// JobScheduler.java 采用短作业优先,且只在已到达的作业中选择 public void dispatch(int currentTick) { JobControlBlock best = null; for (JobControlBlock job : jobQueue) { if (job.getArrivalTime() <= currentTick && job.getState() == JobState.HOLD) { if (best == null || job.getCpuTimeTotal() < best.getCpuTimeTotal()) { best = job; } } } if (best != null && memoryManager.hasFreePartition(best.getRequiredMemory())) { // 内存预检通过才创建PCB PCB pcb = new PCB(best.getPid(), best.getRequiredMemory(), best.getCpuTimeTotal()); pcb.setState(ProcessState.READY); pcb.setPriority(best.getPriority()); readyQueue.add(pcb); best.setState(JobState.CREATED); } // 内存不足时,best保持HOLD状态,等待下一轮再试 }

有几处需要注意。memoryManager.hasFreePartition()只是判断存在足够大的空闲块,不代表一定分配成功——首次适配(First Fit)可能遇到磁盘碎片导致分配不到。所以要加上“分配失败就继续留在后备队列”的回退逻辑。best用局部变量而不是直接操作队列,是为了把“选择”和“创建”两个动作解耦,后续换回FCFS时只需改选则逻辑,不用动PCB创建和内存分配代码。作业的整型比较直接用了getCpuTimeTotal(),课设场景下无需处理空指针,但如果队列里可能混入空对象,需要加非空判断。

3.2 进程调度器Scheduler:按优先级调度并处理时间片轮转

几乎所有操作系统课设的进程调度要求都沾点“时间片轮转 + 优先级”:纯时间片轮转太基础,纯优先级又会饿死低优先级进程。我用的方案是动态优先级老化(Aging)机制——每次未选中运行的进程,优先级数字减1(数字越小优先级越高),相当于等待越久优先级越高。

优先级调度的核心实现思路是:遍历就绪队列,找到优先级最高的进程(数值最小),分配CPU给该进程运行一个时间片,若该进程时间片用完还没完成,则重新放回就绪队列末尾等待下次调度。

// ProcessScheduler.java 动态优先级+时间片轮转 public void schedule(int timeSlice) { if (currentProcess != null) { // 当前有进程在运行 currentProcess.setCpuTimeUsed( currentProcess.getCpuTimeUsed() + timeSlice); if (currentProcess.getCpuTimeUsed() >= currentProcess.getCpuTimeTotal()) { // 进程已运行完毕 memoryManager.release(currentProcess); // 回收内存 currentProcess.setState(ProcessState.EXIT); currentProcess = null; } else { // 时间片用完但未完成,回到就绪队列末尾 readyQueue.add(currentProcess); currentProcess.setState(ProcessState.READY); currentProcess = null; // 关键:先置空再选下一次 } } // 从就绪队列选一个优先级最高的进程 if (currentProcess == null && !readyQueue.isEmpty()) { PCB next = selectHighestPriority(); next.setState(ProcessState.RUNNING); currentProcess = next; } // 老化:就绪队列中每个进程优先级数字减1 for (PCB p : readyQueue) { p.setPriority(p.getPriority() - 1); } }

当时间片用完时,先判断是否运行完毕,否则放回队列末尾——这个“先判断完成再放回末尾”的顺序就是时间片轮转的精髓。进程A运行20个时间片、时间片设为5时,它应该被切开4次,每次运行后重新排队;如果先放回队列再判断完成任务,就会出现“已经还能再跑却提前退出”的边界问题。老化放在最后做,保证下一轮选择前所有就绪进程的优先级都更新过。

3.3 阻塞与唤醒:用BlockQueue模拟I/O等待

进程阻塞是这次课设里最容易被做浅的部分。很多人的实现只是“打印一行:进程阻塞了”,然后直接放到就绪队列继续跑——那就丢了灵魂。正确的做法是:指定若干进程在运行一段时间后,需要模拟I/O操作,进入阻塞队列;阻塞中的进程不参与CPU调度;阻塞的持续时间到了(如5个时间片)才被系统唤醒并回到就绪队列。

阻塞队列也需要一个单独的状态管理类,我来演示阻塞判定与唤醒逻辑的Java实现:

// BlockManager.java 维护阻塞队列并检查唤醒条件 public void checkWaitAndBlock(PCB pcb) { // 旧逻辑已运行时间达到预设的阻塞阈值,且非阻塞状态 if (pcb.getCpuTimeUsed() >= pcb.getBlockThreshold() && pcb.getState() == ProcessState.RUNNING) { pcb.setState(ProcessState.BLOCKED); pcb.setBlockReason("I/O请求"); pcb.setRemainBlockTime(pcb.getBlockDuration()); blockedQueue.add(pcb); } } public void tickBlocked() { Iterator<PCB> it = blockedQueue.iterator(); while (it.hasNext()) { PCB pcb = it.next(); pcb.setRemainBlockTime(pcb.getRemainBlockTime() - 1); if (pcb.getRemainBlockTime() <= 0) { // 阻塞完毕,唤醒 pcb.setState(ProcessState.READY); pcb.setBlockReason(null); readyQueue.add(pcb); it.remove(); } } }

阻塞阈值(多少时间片后触发I/O)和阻塞时长(挂起多少个时间片)直接影响进程调度的走势,答辩情调时候建议把这两个值纳入测试数据的参数配置,例如某进程前30个时间片不阻塞,随后阻塞5个时间片。迭代器删除元素是关键点——不能在遍历时直接用blockedQueue.remove(),否则会抛ConcurrentModificationException后程序卡死。

4. 内存管理的落地:分区分配、回收、地址转换与边界避坑

4.1 动态分区分配:首次适配与最佳适配的实现对比

上课时讲过最佳适配(Best Fit)分配最省空间,课设答辩时问“为什么你选最佳适配”却答不上来的人比比皆是。此处可用数据说话。若作业申请内存序列为:作业A需200KB、作业B需100KB、作业C需300KB,内存总量512KB,分区情况在不同算法下的表现:

  • 首次适配(First Fit):从低地址找第一个够大的空闲块分配,速度快,但会产生较多小碎片。
  • 最佳适配(Best Fit):遍历所有空闲块,选刚好能放下的最小块分配,浪费最小,但每次都要全表扫描。

我通常的做法是两种都实现,通过一个枚举类型切换,测试时对比碎片率。

// MemoryManager.java 实现两种适配算法 public MemoryBlock findFreePartition(int size, FitType type) { MemoryBlock bestBlock = null; for (MemoryBlock block : partitions) { if (!block.isFree() || block.getSize() < size) continue; if (type == FitType.FIRST_FIT) { // 首次适配:直接返回第一个能放下的 return block; } else if (type == FitType.BEST_FIT) { // 最佳适配:找最小的“足够大”块 if (bestBlock == null || block.getSize() < bestBlock.getSize()) { bestBlock = block; } } } return bestBlock; } public boolean allocate(PCB pcb, FitType type) { MemoryBlock block = findFreePartition(pcb.getRequiredMemory(), type); if (block == null) return false; // 分配成功:若块大于需求,则分裂为“占用块 + 新空闲块” int remaining = block.getSize() - pcb.getRequiredMemory(); block.setFree(false); block.setOwner(pcb); pcb.setAllocatedBase(block.getBase()); pcb.setAllocatedSize(pcb.getRequiredMemory()); if (remaining > 0) { MemoryBlock freeBlock = new MemoryBlock( block.getBase() + pcb.getRequiredMemory(), remaining, true); partitions.add(partitions.indexOf(block) + 1, freeBlock); } return true; }

代码里最关键的是remaining > 0的判断。分出的空闲块要求base = block.getBase() + pcb.getRequiredMemory(),这是保持地址连续性。如果remaining小于某个下限——比如16KB——就不要分裂了,直接整块都给这个进程,否则碎片过多。两种算法在课设规模(512KB、几十个分区)下性能差异基本肉眼不可见,重点在于你理解每次分配的路径。

4.2 回收内存时必做的“相邻合并”,以及最容易漏掉的碎片

内存回收是课设中bug的高发区。很多实现把释放逻辑写成“找到块改回free=true”就结束了,结果跑20个作业后剩余空闲分区越来越多,每个却都小于最小作业需求——内存碎片化。真正的回收必须检查前后相邻块:如果前一块或后一块也是空闲,就要合并为一个更大的空闲块,更新基址和大小,防止碎片。

// 释放PCB占用的内存,并与前后空闲块合并 public void release(PCB pcb) { for (int i = 0; i < partitions.size(); i++) { MemoryBlock block = partitions.get(i); if (block.getOwner() == pcb) { block.setFree(true); block.setOwner(null); // 与后一块合并 if (i + 1 < partitions.size() && partitions.get(i + 1).isFree()) { MemoryBlock next = partitions.get(i + 1); block.setSize(block.getSize() + next.getSize()); partitions.remove(i + 1); // 后块从列表移除 } // 与前一空闲块合并(注意合并后索引变化) if (i > 0 && partitions.get(i - 1).isFree()) { MemoryBlock prev = partitions.get(i - 1); prev.setSize(prev.getSize() + block.getSize()); partitions.remove(i); // 合并到prev,当前块被移除 } break; } } }

代码的坑藏在“分三段”的边界条件里。先处理与后块合并,再处理与前块合并,是因为与前块合并后i指向的元素被移除,索引会偏移。block.getOwner() == pcb用引用比较,而不是重新查找进程的allocatedBase——保证同一个PCB正确匹配。如果先与前块合并,后块合并时要重新定位相邻索引,极容易越界。

4.3 地址转换与内存不足时的阻塞策略

进程读写的地址需要经过逻辑地址到物理地址的转换。课设不需要实现真实的页表,但要用分区基址做个简单的加法:物理地址 = 逻辑地址 + allocatedBase。这个转换通常在模拟CPU执行指令时体现,比如进程内“访问逻辑地址100”时,实际输出“物理地址为 allocatedBase + 100”。

如果内存不足怎么办?这里就必须结合进程阻塞——新作业到达但内存无法满足时,不能丢弃,应该让该作业在后台队列继续等待,直到有进程退出释放内存。这个逻辑链:作业调度选中最短作业后,发现内存不足 → 该作业保持Hold状态 → 等待其他进程Exit后,内存管理器释放空间 → 下一轮扫描时该作业被重新选中。

为让这条路更清晰,我建议在内存管理器中维护一个“等待内存队列”:

// 等待内存的作业队列:内存不足时挂起,释放后唤醒 private Queue<PCB> waitingMemoryQueue = new LinkedList<>(); public boolean tryAllocate(PCB pcb) { boolean success = allocate(pcb, FitType.BEST_FIT); if (!success) { pcb.setState(ProcessState.BLOCKED); pcb.setBlockReason("等待内存"); waitingMemoryQueue.add(pcb); } return success; } public void afterRelease() { // 每次释放内存后尝试唤醒等待内存的进程 while (!waitingMemoryQueue.isEmpty()) { PCB pcb = waitingMemoryQueue.peek(); if (allocate(pcb, FitType.BEST_FIT)) { pcb.setState(ProcessState.READY); readyQueue.add(pcb); waitingMemoryQueue.poll(); } else { break; } } }

这种把“内存不足”建模为一种阻塞原因的做法,自然融入了进程阻塞模块,答辩时如果有人问你“内存和阻塞怎么结合”,这一条就够了。

5. 用JUnit测试状态迁移和调度序列:三组测试用例的设计方法

5.1 课设的验收关键:状态迁移的时序如何验证

敲完代码先别急着写界面,课设验收最看重的是“你能证明它对了”。JUnit用起来比在main里打印日志专业得多。这里展示一个测试内存分配、进程状态流转的最小JUnit测试类,以及无需JUnit的纯main验证法(很多人不装Maven依赖也能跑)。

// MemoryTest.java 核心状态迁移验证 public class ScheduleTest { @Test public void testJobToReadyToRunning() { OSKernel kernel = new OSKernel(); JobScheduler js = kernel.getJobScheduler(); // 构造一个0时刻到达,需CPU 5个时间片的作业 js.addJob(new JobControlBlock(1, 200, 5, 0)); kernel.start(10); // 运行10个tick // 验证第10个tick后,该进程要么已完成,要么在就绪或运行 PCB pcb = kernel.findProcessById(1); assertNotNull(pcb); assertTrue(pcb.getState() == ProcessState.EXIT || pcb.getState() == ProcessState.RUNNING); } }

真正写测试时,要覆盖三条关键的链路:作业从到达到就绪,时间片切换时从RUNNING回READY(观察状态序列和cpuTimeUsed累加值),以及阻塞进程在blockTime耗尽后回到READY。每一次断言都要检查状态字段,因为状态枚举是全局唯一的判定依据——只打印日志而不校验状态,程序跑完也不知道对错。

5.2 测试数据的设计:用“批量任务生成器”替代手工写10个作业

手动在main里一个个new作业,容易写错到达时间和内存需求,也难复现。我一般写一个TaskGenerator:随机生成指定数量的作业,按指数分布设定到达间隔,内存需求控制在64KB到256KB之间,CPU总时间控制在5到30个时间片。这样能模拟课堂演示的“短作业居多”场景,同时保证几个大作业穿插其中,让内存分配有压力。

// TaskGenerator.java 生成可复现的测试作业序列 public static List<JobControlBlock> generate(int count, long seed) { Random rand = new Random(seed); // 固定种子,可复现 List<JobControlBlock> jobs = new ArrayList<>(); int arrivalTime = 0; for (int i = 1; i <= count; i++) { int memory = 64 + rand.nextInt(193); // 64~256KB int cpuTime = 5 + rand.nextInt(26); // 5~30个时间片 arrivalTime += rand.nextInt(5); // 间隔0~4个tick jobs.add(new JobControlBlock(i, memory, cpuTime, arrivalTime)); } return jobs; }

固定种子让每次运行序列一致,答辩时数据有可复现性。64 + rand.nextInt(193)生成的作业内存需求在64~256KB之间,如果设置512KB内存,最多同时容纳2~8个作业,模拟出饱满的效果。

表:SJF与FCFS在相同10个作业下的平均周转时间对比(CPU总时间片=70,内存=512KB)

算法完成作业数平均周转时间(tick)平均等待时间(tick)
FCFS1038.228.4
SJF1026.716.9

注意表格里的数据是固定的,如果改动作业序列必须重新计算。差异背后体现SJF的优势,但SJF也让长作业等待更久,加老化机制能缓解、不能根除——这个权衡要在答辩时说清楚。最后生成一个每次运行都固定输出的“时序打印器”,我可以在这里把它写进去。

6. 控制台输出的排版技巧与答辩时最加分的验证手法

6.1 用表格化输出让每一步状态变化都一目了然

控制台程序最容易出现的场面是:print语句堆得像山一样,几千行滚过去,老师一句“哪个是哪个”直接把你问住。我的做法是把输出收敛到一个统一格式:每轮tick打印一行概要,包含当前运行进程、就绪队列长度、阻塞队列长度、内存使用率。重要事件(进程创建、分配内存、阻塞、唤醒、退出)则单独打印分隔行。

// 每轮tick的状态概要 System.out.printf("[tick %3d] 运行: %3s | 就绪: %2d个 | 阻塞: %2d个 | 内存: %3d%% | 已完成: %2d个%n", tick, current == null ? "--" : "P" + current.getPid(), readyQueue.size(), blockedQueue.size(), memoryManager.usagePercent(), completedCount);

其中%3d对齐宽度,%3s处理空指针,%%输出百分号。这样生成的输出像仪表盘,拖到最后几行就能看到整体运行轨迹,内存使用率的变化曲线非常直观。事件行用标记开头,比如==> P3 阻塞,原因: I/O,扫描一眼就能找到关键节点。

6.2 人工“照妖镜”:把调度序列导到文本,再手动画甘特图

答辩时最能加分的整理方式,是timeline输出功能——把所有关键事件按时间顺序导出成文本,格式像日志一样:tick 12: P1进入就绪,分配内存[0~200]、tick 15: P1阻塞,原因I/O,预计5个tick后唤醒、tick 20: P1唤醒,回到就绪队列。然后你按这个日志手工画一张甘特图(或直接在纸上画进程切换时序)。

这套验证有两个价值。一是自查状态迁移是否满足逻辑,很多测试断言不到的死循环和状态漏改,人眼扫一遍日志就能发现;二是答辩时直接投影这张时间线,比任何口头解释都有说服力——老师说“你演示一下P1从阻塞到唤醒的过程”,你指着一行行输出让他看,这就是所谓“课设的照妖镜”。

6.3 最后一个技巧:把参数全部集中到配置文件,一分钟换一套实验数据

把作业数量、内存大小、时间片长度、阻塞阈值、调度算法全部硬编码在代码里,是课设中最影响体验的做法。每次改参数都要重新编译,而且非常容易把代码搞乱。我习惯写一个config.properties:

totalTicks=300 memorySize=512 timeSlice=5 fitType=BEST_FIT scheduleAlgorithm=SJF ioInterval=3

然后在启动时加载:

Properties props = new Properties(); try (InputStream in = new FileInputStream("config.properties")) { props.load(in); } catch (IOException e) { e.printStackTrace(); // 读不到配置文件则使用默认值 } int memorySize = Integer.parseInt(props.getProperty("memorySize", "512")); int timeSlice = Integer.parseInt(props.getProperty("timeSlice", "5"));

答辩现场换参数演示时,只需改配置文件再重启,就能展示不同调度算法、不同时间片下的系统表现。把“实验对比”从代码重构降低到改配置的级别,这是从课设代码到“能演示的系统”的质变,也是我反复调校这套代码留下的最值得养成的习惯。另外记得FileInputStream要关流,项目里全局只此一处加载配置,无需担心并发读写。希望帮到你。

本文还有配套的精品资源,点击获取