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.
Table of Contents
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.