Home/Learn/Operating Systems/Preemptive vs Non-Preemptive Scheduling

Preemptive vs Non-Preemptive Scheduling

Beginner
CPU Scheduling

Non-preemptive scheduling lets a process run until it voluntarily yields; preemptive scheduling allows the OS to forcibly interrupt a running process via a timer interrupt, enabling better responsiveness but requiring synchronization.

Overview

Non-preemptive (cooperative) scheduling: once a process gets the CPU, it keeps it until it voluntarily gives it up — either by requesting I/O, calling yield(), or terminating. Simple to implement, no race conditions on kernel data structures, but a misbehaving process can monopolize the CPU. Preemptive scheduling: the OS uses a hardware timer interrupt to periodically reclaim the CPU from the running process, regardless of whether it is willing to yield. This enables responsive multitasking — short tasks get CPU time even when a long task is running. However, preemption can interrupt a process at any instruction, including in the middle of updating shared data structures, which is why synchronization (mutexes, semaphores) is essential. All modern OS (Linux, Windows, macOS) are preemptive.

How Timer Interrupts Enable Preemption

The OS programs a hardware timer (PIT or APIC) to fire an interrupt at regular intervals (the tick rate — typically 100 Hz to 1000 Hz on Linux, i.e., every 1–10 ms). When the timer fires, the CPU switches from user mode to kernel mode, the kernel's interrupt handler runs, checks if the current process has exhausted its time slice, and if so, calls the scheduler to pick the next process. The running process had no say in this — it was preempted.

Java — preemption causing race condition + synchronized fix
// Preemption and synchronization — why volatile and synchronized are needed
// Without preemption: single-threaded, no races. With preemption: races everywhere.

// Race condition due to preemptive scheduling:
class Counter {
    private int count = 0;

    // NOT thread-safe: preemption can interrupt between read and write
    public void incrementUnsafe() {
        // These 3 bytecode instructions are NOT atomic:
        // 1. GETFIELD count        (read count into register)
        // ← timer interrupt fires HERE, another thread runs and also increments
        // 2. IADD 1                (add 1 in register)
        // 3. PUTFIELD count        (write back — OVERWRITES other thread's increment)
        count++;
    }

    // Thread-safe: synchronized prevents preemption from causing inconsistency
    public synchronized void incrementSafe() {
        count++;   // only one thread at a time — mutual exclusion
    }

    // Fastest: atomic CAS operation (hardware-level, no lock needed)
    private final AtomicInteger atomicCount = new AtomicInteger(0);
    public void incrementAtomic() {
        atomicCount.incrementAndGet();   // atomic compare-and-swap
    }
}

Cooperative vs Preemptive: Java Examples

Java threads are preemptively scheduled by the OS. Voluntary yields can be done with Thread.yield() (hint to scheduler) or Thread.sleep(0). In non-preemptive environments (like early Windows 3.x or Node.js event loop), you had to call cooperative yield points explicitly. Virtual threads (Java 21) use cooperative scheduling internally — they yield at blocking points (I/O, sleep), but the carrier thread itself is preemptively scheduled.

Java — Thread.yield(), volatile flag, and virtual thread cooperative unmounting
// Thread.yield() — cooperative hint (not guaranteed)
Thread cpuHog = new Thread(() -> {
    for (long i = 0; i < Long.MAX_VALUE; i++) {
        if (i % 1_000_000 == 0) {
            Thread.yield();   // politely suggest: let other threads run
            // OS may ignore this — just a hint
        }
    }
});

// volatile — ensures preemption-safe visibility of a flag across threads
// Without volatile, a thread may cache the value in a CPU register
volatile boolean running = true;

Thread worker = new Thread(() -> {
    while (running) {   // reads from main memory — not cached register
        // do work
    }
    System.out.println("Worker stopped");
});

worker.start();
Thread.sleep(100);
running = false;   // visible to worker thread immediately due to volatile
worker.join();

// Non-preemptive cooperative model (analogous to Node.js event loop)
// Java's virtual thread parking at I/O = cooperative yield on carrier thread
Thread vt = Thread.ofVirtual().start(() -> {
    // This blocks the virtual thread but cooperatively unmounts from carrier
    Thread.sleep(100);  // virtual thread parks → carrier picks up next virtual thread
});

Key Points to Remember

  • 1Non-preemptive: process holds CPU until it voluntarily yields — simple but risks monopolization.
  • 2Preemptive: OS timer interrupt forcibly reclaims CPU — better responsiveness, requires synchronization.
  • 3All modern OS (Linux, Windows, macOS) use preemptive scheduling.
  • 4Preemption creates race conditions when threads share data — use synchronized, volatile, or AtomicXxx.
  • 5Thread.yield() is a cooperative hint to the scheduler, but the OS may ignore it.
  • 6Java virtual threads use cooperative scheduling internally but run on preemptively scheduled carrier threads.

Interview Questions

Sign in to ask Aria
1

What is the difference between preemptive and non-preemptive scheduling?

EasyAmazon
2

Why does preemptive scheduling require synchronization but cooperative scheduling does not?

MediumGoogle
3

How does a hardware timer interrupt implement preemptive scheduling?

HardMicrosoft
4

What does the volatile keyword in Java protect against?

MediumFlipkart

Ask Aria about Preemptive vs Non-Preemptive Scheduling

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…