Thread Context Switching

About

Context Switching is the process where the CPU switches from executing one thread (or process) to another. It involves saving the current state of the running thread and loading the state of the next thread.

In Java, context switching is a fundamental part of multithreading and concurrency, allowing multiple threads to share CPU time efficiently. However, excessive switching can introduce overhead, reducing performance.

How Context Switching Works ?

When the CPU switches from one thread to another, it performs these steps:

  1. Save the execution state of the current thread (Program Counter, Registers, Stack Pointer).

  2. Load the saved state of the new thread that is to be executed.

  3. Resume execution of the newly loaded thread from where it left off.

The entire process happens very fast (in milliseconds or microseconds), but it still introduces some overhead.

Why Does Context Switching Occur ?

1. Preemptive Multitasking (Time-Slicing)

  • Java follows preemptive scheduling, meaning that the JVM scheduler allocates a fixed time slice for each thread.

  • Once the time slice is over, the CPU switches to another thread, even if the previous one has not finished execution.

2. Thread Priority-Based Switching

  • The JVM scheduler prioritizes high-priority threads, causing context switching when a higher-priority thread becomes available.

  • However, thread priorities are not guaranteed and depend on the OS-level thread scheduling policy.

3. Blocking Operations

  • If a thread performs an I/O operation (file read, network call, database access, etc.), the OS may pause it and switch to another thread while waiting for the operation to complete.

4. Synchronization & Locks

  • When a thread waits for a lock, it is put in a waiting state, and the CPU switches to another thread that is ready to execute.

5. Manual Thread Sleep or Yield

  • Calling Thread.sleep(time) or Thread.yield() forces the CPU to switch to another thread voluntarily.

Example of Context Switching

  • The output does not follow a strict order because of context switching.

  • Each thread runs independently, and the JVM scheduler decides when to switch threads.

class Task extends Thread {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is executing: " + i);
            try {
                Thread.sleep(100); // Simulate some work
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ContextSwitchingExample {
    public static void main(String[] args) {
        Thread t1 = new Task();
        Thread t2 = new Task();
        Thread t3 = new Task();

        t1.start();
        t2.start();
        t3.start();
        
        /* Output may vary
        Thread-0 is executing: 1
        Thread-1 is executing: 1
        Thread-2 is executing: 1
        Thread-0 is executing: 2
        Thread-1 is executing: 2
        Thread-2 is executing: 2
        ...
        */
    }
}

Types of Context Switching

1. Process Context Switching

  • Happens when the OS switches between processes.

  • More expensive due to separate memory space management.

2. Thread Context Switching

  • Happens when switching between threads of the same process.

  • Less expensive than process switching, as threads share memory.

Performance Overhead of Context Switching

Context switching is necessary for multitasking but introduces performance costs:

  1. CPU Overhead – Saving and restoring registers, stack, and program counters.

  2. Cache Invalidation – CPU cache may need to be reloaded when switching between threads.

  3. Locking Issues – If multiple threads compete for locks, frequent switching may lead to higher contention.

Ways to Reduce Context Switching in Java

1. Use Fewer Threads if Possible

  • Too many threads increase CPU switching overhead.

  • Use Thread Pools (ExecutorService) to manage thread allocation efficiently.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // Fixed thread pool of 3
        for (int i = 0; i < 5; i++) {
            executor.execute(new Task());
        }
        executor.shutdown();
    }
}

2. Use Lock-Free Data Structures

  • Avoid synchronized blocks when possible, as they increase thread waiting and switching.

  • Use concurrent collections like ConcurrentHashMap instead of synchronized Map.

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentExample {
    public static void main(String[] args) {
        ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
        map.put(1, "Thread Safe");
    }
}

3. Use Thread.yield() Wisely

  • Thread.yield() allows the JVM to switch to another thread but does not guarantee switching.

class YieldExample extends Thread {
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(Thread.currentThread().getName() + " running");
            Thread.yield();
        }
    }
}

public class YieldDemo {
    public static void main(String[] args) {
        YieldExample t1 = new YieldExample();
        YieldExample t2 = new YieldExample();

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

4. Prefer ReentrantLock Over synchronized

  • ReentrantLock provides better control over locking mechanisms and can reduce unnecessary context switching.

import java.util.concurrent.locks.ReentrantLock;

class ReentrantExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doWork() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " acquired lock.");
        } finally {
            lock.unlock();
        }
    }
}

public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        Thread t1 = new Thread(example::doWork);
        Thread t2 = new Thread(example::doWork);
        t1.start();
        t2.start();
    }
}

Last updated

Was this helpful?