Home ยป Java Thread Scheduler Tutorial

Java Thread Scheduler Tutorial

In Java, Thread Scheduler is part of the JVM (Java Virtual Machine) responsible for determining which thread runs at any given time. The Thread Scheduler uses thread priorities and the underlying operating system's scheduling mechanisms to decide how threads are managed and executed.

While Java does not provide direct control over the Thread Scheduler, you can influence how threads behave using thread priorities and other concurrency control mechanisms. It's important to note that the actual scheduling behavior is platform-dependent, meaning different operating systems may handle thread scheduling differently.

In this tutorial, we will explore how Java threads work with the Thread Scheduler, how to use thread priorities, and how to manage thread behavior with code examples.

1. Introduction to Thread Scheduling

Java provides a preemptive scheduling model where each thread competes for CPU time based on thread priority. The thread scheduler decides which thread runs at any given time, and threads may be:

Running: Currently executing.
Runnable: Ready to run, waiting for CPU time.
Blocked/Waiting: Paused, waiting for a resource or event.
Java threads can be preempted (interrupted by higher-priority threads), or they may yield (voluntarily give up the CPU).

2. Setting Thread Priority

Each thread in Java has a priority value between 1 and 10, where:

Thread.MIN_PRIORITY = 1 (lowest priority)
Thread.NORM_PRIORITY = 5 (default priority)
Thread.MAX_PRIORITY = 10 (highest priority)
Higher-priority threads are more likely to be chosen by the scheduler to run first. However, thread priorities are just hints to the scheduler, and actual behavior can vary based on the operating system.

Example of Setting Thread Priority:

public class ThreadPriorityExample {
    public static void main(String[] args) {
        // Creating two threads with different priorities
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) { System.out.println("Thread 1 (Priority: " + Thread.currentThread().getPriority() + ") - Count: " + i); } }); Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 (Priority: " + Thread.currentThread().getPriority() + ") - Count: " + i);
            }
        });

        // Setting thread priorities
        thread1.setPriority(Thread.MIN_PRIORITY);  // Lowest priority (1)
        thread2.setPriority(Thread.MAX_PRIORITY);  // Highest priority (10)

        // Starting the threads
        thread1.start();
        thread2.start();
    }
}

Explanation:

thread1 is set to the lowest priority using setPriority(Thread.MIN_PRIORITY).
thread2 is set to the highest priority using setPriority(Thread.MAX_PRIORITY).
The output may show that thread2 is more likely to finish before thread1, but this behavior can vary depending on the system.

Output (may vary based on OS and JVM):

Thread 2 (Priority: 10) - Count: 1
Thread 2 (Priority: 10) - Count: 2
Thread 2 (Priority: 10) - Count: 3
Thread 2 (Priority: 10) - Count: 4
Thread 2 (Priority: 10) - Count: 5
Thread 1 (Priority: 1) - Count: 1
Thread 1 (Priority: 1) - Count: 2
Thread 1 (Priority: 1) - Count: 3
Thread 1 (Priority: 1) - Count: 4
Thread 1 (Priority: 1) - Count: 5

3. The yield() Method

The Thread.yield() method is a hint to the scheduler that the current thread is willing to yield (pause) and allow other threads to execute. This is useful for creating more cooperative multithreading but does not guarantee any specific behavior.

Example of Using yield():

public class YieldExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) { System.out.println("Thread 1 (yielding)"); Thread.yield(); // Yielding to allow other threads to execute } }); Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 2 (running)");
            }
        });

        // Starting both threads
        thread1.start();
        thread2.start();
    }
}

Explanation:

Thread.yield() allows thread1 to suggest that other threads (e.g., thread2) run.
thread1 will give up the CPU, but there is no guarantee that thread2 will immediately runโ€”it depends on the scheduler.

Output (may vary):

Thread 1 (yielding)
Thread 2 (running)
Thread 1 (yielding)
Thread 2 (running)
Thread 2 (running)
Thread 2 (running)
Thread 2 (running)
Thread 1 (yielding)

4. The sleep() Method

The Thread.sleep() method pauses the execution of the current thread for a specified period (in milliseconds). During this time, the thread is in a sleeping state and does not consume CPU resources.

Example of Using sleep():

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread is running: Count " + i);
                try {
                    Thread.sleep(1000);  // Sleep for 1 second
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Starting the thread
        thread.start();
    }
}

Explanation:

The Thread.sleep(1000) pauses the thread for 1 second between iterations.
The InterruptedException must be handled in case the thread is interrupted while sleeping.

Output (1-second delay between each print):

Thread is running: Count 1
Thread is running: Count 2
Thread is running: Count 3
Thread is running: Count 4
Thread is running: Count 5

5. The join() Method

The join() method allows one thread to wait for another thread to complete before continuing its execution. This is useful when you need to ensure that a certain thread finishes before others.

Example of Using join():

public class JoinExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) { System.out.println("Thread 1 running..."); try { Thread.sleep(500); // Sleep for 0.5 second } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 waiting for thread 1 to finish...");
            try {
                thread1.join();  // Wait for thread1 to finish
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 started after thread 1.");
        });

        // Start thread1 and thread2
        thread1.start();
        thread2.start();
    }
}

Explanation:

thread2 will wait for thread1 to finish before it proceeds, thanks to the join() method.

Output:

Thread 2 waiting for thread 1 to finish...
Thread 1 running...
Thread 1 running...
Thread 1 running...
Thread 2 started after thread 1.

6. Thread State

Java threads have different states, and these states represent the lifecycle of a thread:

NEW: A thread that has not yet started.
RUNNABLE: A thread that is ready to run or currently running.
BLOCKED: A thread that is blocked and waiting to acquire a lock.
WAITING: A thread that is waiting indefinitely for another thread to perform an action.
TIMED_WAITING: A thread that is waiting for a specified time.
TERMINATED: A thread that has finished execution.

Example of Checking Thread State:

public class ThreadStateExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is running");
        });

        // Checking thread state before starting
        System.out.println("Thread state: " + thread.getState());  // NEW

        // Start the thread
        thread.start();

        // Checking thread state after starting
        System.out.println("Thread state: " + thread.getState());  // RUNNABLE (may vary)

        // Sleep to ensure the thread finishes
        try {
            Thread.sleep(100);  // Wait for the thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Checking thread state after execution
        System.out.println("Thread state: " + thread.getState());  // TERMINATED
    }
}

Explanation:

You can check the state of a thread using the getState() method.

Output (may vary depending on thread execution):

Thread state: NEW
Thread state: RUNNABLE
Thread is running
Thread state: TERMINATED

7. Thread Yielding vs. Sleeping

While both yield() and sleep() affect the thread's execution, there are key differences:

yield(): Suggests the thread scheduler give other threads a chance to run, but the current thread may continue running if no other thread is ready.
sleep(): Pauses the current thread for a specified time and guarantees that the thread will not run during that period.

8. Thread Scheduler Behavior (Platform Dependency)

The Java Thread Scheduler relies on the underlying operating system's scheduling algorithm, which can differ between platforms (e.g., Windows, Linux, macOS). Therefore, thread priority and other scheduling behaviors can vary.

Windows: Typically uses a time-slicing mechanism where each thread is given a time slot (quantum) to execute.
Linux: Uses fair scheduling and prioritizes based on various factors, including thread priorities and resource needs.
While Java provides APIs to influence thread behavior, the exact scheduling is not guaranteed and depends on the JVM and OS.

Conclusion

Java's Thread Scheduler determines the execution order of threads in a concurrent environment, but it offers limited direct control over how threads are scheduled. Key concepts include:

Thread Priority: You can suggest the relative importance of a thread using priorities, but behavior is platform-dependent.
yield(): A way to suggest that the current thread allows others to run.
sleep(): Pauses a thread for a specific time period, relinquishing the CPU.
join(): Ensures one thread waits for another thread to finish before continuing.
Thread States: Understanding the lifecycle of a thread is critical to managing concurrency.

By leveraging these tools, you can effectively manage thread execution and influence the behavior of the JVM's thread scheduler, although the final decisions are made by the underlying operating system.

You may also like