JVM 与 OS 调度之间的爱恨情仇

18 阅读4分钟

OS 的调度有很多种 先来先服务 FCFS、短作业优先 SJF、时间片轮转 RR、优先级调度. .. ...

我们通过模拟 OS 的 “时间片轮转调度(RR)” 来了解对应的 RR 算法,

接下来看看,用 JDK 的请求,能否干预计算机里的基石 OS 自己的算法

使用 Java 模拟 OS 中的 RR 算法

RR 算法简述

时间片轮转算法(RR)操作系统(OS) 中的一个进程/作业调度算法,RR 锁定时间,每一个进程只能占用 CPU 一个固定的时间,这样子使得每一个进程都能够在 CPU 上执行。

算法思想:轮流让各个进程执行一个时间片(如 50ms),若进程在时间片中没有执行完,则剥夺处理机,将进程重新放置回就绪队列的队尾。

使用 Java 模拟 RR

声明

当前使用的模拟指的是,我们会创建 处理机/CPU、 就绪队列、 PCB... ...

这一切都会运行在我们的 JVM 上,当模拟 RR 开始时。

代码实现

import java.util.LinkedList;
import java.util.Queue;

class MyProcess {
    String name;
    int burstTime; // 需要运行的总时间
    int remainingTime; // 剩下没跑完的时间

    public MyProcess(String name, int burstTime) {
        this.name = name;
        this.burstTime = burstTime;
        this.remainingTime = burstTime;
    }
}

public class RoundRobinSimulation {
    public static void main(String[] args) {
        // 假设时间片是 2ms
        int timeQuantum = 2;

        Queue<MyProcess> queue = new LinkedList<>();
        queue.add(new MyProcess("P1", 5));
        queue.add(new MyProcess("P2", 3));
        queue.add(new MyProcess("P3", 1));

        int currentTime = 0;

        while (!queue.isEmpty()) {
            MyProcess p = queue.poll(); // 取出一个进程

            System.out.println("当前时间: " + currentTime + " | 正在运行: " + p.name);

            // 1. 如果 remainingTime > timeQuantum -> 跑 timeQuantum 时间,然后重新加入队列
            if (p.remainingTime > timeQuantum) {
                currentTime += timeQuantum;

                p.remainingTime -= timeQuantum;

                queue.add(p);
            } else {
                // 2. 如果 remainingTime <= timeQuantum -> 跑完,任务结束
                currentTime += p.remainingTime;

                p.remainingTime = 0;

                System.out.println("当前时间: " + currentTime + " | 进程" + p.name + "已经终止");
            }
        }
    }
}

运行结果

结论

事实上 OS 中 CPU 在执行途中每个时间片会中断当前的进程,或者进程在执行完成后会主动发起退出处理机/CPU 进入终止态,释放 PCB 占用的内存

单使用 Java 模拟并不能模拟出 OS 对于底层硬件的控制操作,只能进行一段“拙劣”的模仿

此外,我们用 Java 的 queue.poll() 和 add() 只是简单的内存操作

但在真实的 OS 中,这一步对应的是上下文切换 (Context Switch)

OS 需要保存上一个进程的寄存器、栈信息(存档),并读取下一个进程的信息(读档)

这其实是非常消耗 CPU 性能的,这也是为什么时间片不能设置得太小的原因

而如果时间片设置的过于大,则会导致所有的进程都能够运行完成,会变成先来先服务算法,那么时间片轮转算法就没有意义了

使用 Java 中的 API 干预 OS 调度

优先级简介

我们知道 OS 的调度机制存在很多的判定属性,其中一项最为关键的判定属性就是“优先级”

在 Java 编程中,存在着 设置优先级(setPriority) 这个方法,那么让我们手动设置优先级,看底层的 OS 是否会会根据我们设置的优先级进行线程的调度

使用 Java 设置优先级干预 OS 调度

代码部分

public class PriorityDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                // 空循环耗时
            }
            System.out.println("🔴 低优先级线程跑完了");
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                // 空循环耗时
            }
            System.out.println("🟢 高优先级线程跑完了");
        });

        t1.setPriority(Thread.MIN_PRIORITY); // 1
        t2.setPriority(Thread.MAX_PRIORITY); // 10

        t1.start();
        t2.start();
    }
}

运行结果

结论

如果我们多运行几次,不难发现:明明我们都已经设置了对应的优先级啊,为什么优先级低的进程还是有先跑完的呢?

答案就藏在 JDK 所处的“态”,

JDK 所处的是 用户态 ----- 用户态不能够直接生成线程

OS 所处的是 内核态 ----- 内核态才是整个计算机的“老大”

你想要干什么都需要内核态拍板,而用户态能做到的只有向内核态这个“老大”提建议、发请求。

而 OS 自己有自己的想法,自己有自己的调度算法,Java 的“优先级”只是给 OS 的一个“建议”

现在的 OS(Linux/Windows)调度非常复杂(CFS 调度、多级反馈队列),它为了防止“饥饿”,不会完全听你的。而且现在的 CPU 是多核的,t1 和 t2 可能在两个核上并行跑,谁快谁慢纯看命

Java 线程调度是不可控的(非确定性),这是并发编程最大的难点

例如在 Linux 系统中,默认使用的是 CFS (Completely Fair Scheduler,完全公平调度器)

它的核心哲学是‘公平’,而不是严格按优先级插队

虽然我们设置了优先级,但在 CFS 眼里,只要低优先级的线程‘饿’得够久,它依然会获得 CPU 时间片

而在 Windows 中,优先级的影响可能会稍微明显一点

这就解释了为什么 Java 的调度是‘非确定性’的