Showing posts with label JVM. Show all posts
Showing posts with label JVM. Show all posts

Thursday, December 11, 2025

Java Thread Priorities Explained: 5 Hidden Truths Every Developer Must Know

 

Most Java developers are familiar with the concept of thread priorities. We learn early on that we can set a value from 1 to 10 to influence which thread gets the CPU's attention. But this seemingly simple concept has surprising nuances that can easily trip up even experienced programmers. The textbook definitions, I've found, often don't tell the whole story.

This article uncovers the most counter-intuitive and impactful truths about how thread priorities really work in Java. We'll move beyond the basics to explore the secrets that can make or break your multi-threaded applications.

1. It's a Priority, Not a Rank: Why 10 is More Important Than 1

A common point of confusion is whether a lower number signifies a higher priority, like a first-place rank in a competition. We are culturally conditioned to think "number one" means the best, but in Java, the opposite is true: 1 represents the minimum priority (Thread.MIN_PRIORITY) and 10 represents the maximum priority (Thread.MAX_PRIORITY).

This reminds me of a story I heard about two illiterate villagers discussing their children's exam results. The first father was distraught, complaining that after spending a fortune on tuition, his son got the "worst rank"—2,500. The second father, beaming with pride, boasted that his obedient son was far better, having achieved the "best performance" with a rank of 148,000! They completely misunderstood that in academic ranks, a lower number is better.

When it comes to Java threads, we must avoid their mistake.

"most of the people are going to feel like this only these are not ranks okay... one is mean priority 10 is Max priority"

Understanding this fundamental rule—that higher numbers mean higher priority—is the crucial first step before we can explore more complex behaviors.

2. The Inheritance Secret: The Default Priority Isn't Always 5

Here's the one that trips up developers on code reviews more than any other: the myth of the universal default priority. Many developers assume that any new thread automatically receives a default priority of 5, or Thread.NORMAL_PRIORITY.

The truth is more nuanced. Only the main thread has a default priority of 5.

All other threads, by default, follow a rule of priority inheritance. A new "child" thread inherits the priority of its "parent" thread—the thread that created it. For example, if you change the main thread's priority to 7 and then create a new thread, that new child will start life with a priority of 7, not 5.

"the default priority only for the main thread is five but for all remaining threats default priority will be inherited from parent to child"

This is a vital secret to know, as assuming every thread starts at NORMAL_PRIORITY can lead to completely wrong assumptions about your application's execution flow.

3. The Scheduler's Gamble: The Truth About Priority Ties

So, a higher-priority thread gets the CPU's attention first, much like a Prime Minister's motorcade gets waved through a traffic signal before anyone else. But what happens when two threads with the exact same priority arrive at the intersection?

When threads of equal priority are competing, there is no guaranteed outcome. The execution order becomes unpredictable and is left entirely to the thread scheduler's implementation. The scheduler might use a round-robin algorithm, giving each a turn, or it might be first-come-first-served. The key is that you, the programmer, cannot and should not rely on a specific behavior.

"if two threats having same priority then... we can't expect exact execution order it depends on thread scheduler"

This serves as an important lesson in the limits of our control. Priorities can influence the scheduler, but they don't provide absolute command over thread execution.

4. The Runtime Ambush: Setting an Invalid Priority

You can change a thread's priority at any time using the setPriority(int p) method. But what happens if you accidentally provide a value outside the legal 1-10 range? The compiler won't warn you. The problem will only surface at runtime, waiting to ambush your application when it's least expected.

When the setPriority() method is called with an invalid value, it throws an IllegalArgumentException. This isn't a benign error; an unhandled IllegalArgumentException can crash a critical thread, leading to data corruption or a service outage.

// This is a valid priority within the 1-10 range.
t.setPriority(7);

// This value is outside the 1-10 range and will cause a runtime error.
t.setPriority(17); // Throws IllegalArgumentException

This is an important "gotcha" to be aware of. Because it's a runtime exception, this bug might go unnoticed during development and only appear in production, making it crucial to validate priority values or catch the exception.

5. The Ultimate Betrayal: Your Operating System Might Just Ignore You

This is the final, most impactful real-world truth: even if you implement thread priorities perfectly in your Java code, they might not work as you expect. I remember the first time I saw this in action. I wrote a simple demo where a high-priority thread (10) should have always printed its output before a low-priority thread (5). I ran it, and the output was completely mixed. My expected output wasn't appearing. "Why is this problem coming?" I wondered.

The reason is that the Java Virtual Machine (JVM) relies on the underlying operating system (OS) for thread management. If the OS doesn't provide robust support for thread priorities, the JVM's instructions are effectively ignored. My demo, running on Windows 7, was a perfect example of this platform-dependent behavior.

"some platforms won't Provide support some platforms won't Provide proper support for thread priorities"

This is a critical warning. You should never rely on thread priorities to control critical application logic or to ensure correctness. They are a hint to the scheduler, not a contract.

Conclusion: A Tool, Not a Guarantee

Thread priorities in Java are a nuanced tool for influencing the thread scheduler, not a strict set of rules that guarantee execution order. As we've seen, their behavior is shaped by inheritance, scheduler implementations, and most critically, the capabilities of the underlying operating system. Relying on them for program correctness is a recipe for unpredictable, platform-dependent bugs.

Knowing that priorities can be so unpredictable, what other concurrency control mechanisms should be the primary tools in a robust multi-threaded application?

Tuesday, December 9, 2025

3 Surprising Facts About Java’s Static Control Flow That Most Developers Get Wrong

 

Most Java developers, when asked where a program starts, will give a confident, one-word answer: main. It’s one of the first things we learn. The public static void main(String[] args) method is the designated entry point for the Java Virtual Machine (JVM). But is it truly the first thing that runs?

Let’s put that assumption to the test. Take a look at the following code and predict its output. No compiling—just a gut reaction.

class Base {
    static {
        m1();
        System.out.println("First static block");
    }

    public static void main(String[] args) {
        m1();
        System.out.println("Main method");
    }

    public static void m1() {
        System.out.println(j);
    }

    static {
        System.out.println("Second static block");
    }

    static int j = 20;
}

If you predicted anything starting with 20, or that the main method would execute first, prepare to have your assumptions challenged. The actual output is 0, First static block, Second static block, 20, Main method. Let's break down why. This article will unravel the mystery by revealing three core truths about Java's static control flow that challenge our common assumptions.

--------------------------------------------------------------------------------

1. Surprise! The main Method is the Last Thing to Run

The biggest misconception about static control flow is that the main method is the starting point. Contrary to this popular belief, when a class contains static members, the main method is actually the final step in a much longer initialization sequence.

The JVM must first prepare the class by loading and initializing all its static components before it can ever call main. This means that by the time main executes, a series of other operations has already been completed.

"most of the people are going to feel sir main method is the starting point of our program execution but strictly speaking in static control flow main method is the last thing which is going to be executed"

--------------------------------------------------------------------------------

2. The Real Execution Order: A Three-Step Process

So, if main isn't the beginning, what is? The JVM follows a strict, predictable three-step process to initialize a class and its static members. Understanding this sequence is the key to predicting the output of our quiz.

  1. Step 1: Identification of static members from top to bottom. First, the JVM performs a complete scan of the class from top to bottom, identifying every static member (variables, methods, and blocks). During this phase, any static variables are assigned their JVM-provided default values (0 for int, null for objects). At this point, the variable enters a "Read Indirectly, Write Only" (RIWO) state, a concept we'll explore in detail in the next section.
  2. Step 2: Execution of static variable assignments and static blocks from top to bottom. Next, the JVM makes a second pass through the class, again from top to bottom. This time, it executes two things in the order they appear: the explicit assignments for static variables (e.g., j = 20) and the code inside any static blocks.
  3. Step 3: Execution of the main method. Only after the identification and execution steps are fully complete does the JVM finally invoke the main method.

With this three-step process, the output of the quiz code becomes clear. Let's trace the execution:

  • Step 1: Identification (Top to Bottom)
    • static { m1(); ... } is identified.
    • public static void main(...) is identified.
    • public static void m1() is identified.
    • static { ... } is identified.
    • static int j is identified. The JVM assigns its default value: j = 0. The variable j is now in its RIWO state.
  • Step 2: Execution (Top to Bottom)
    • First static block executes:
      • m1() is called. m1() prints the value of j. Since j is still in its RIWO state, this indirect read prints its default value. Output: 0
      • System.out.println("First static block") runs. Output: First static block
    • Second static block executes:
      • System.out.println("Second static block") runs. Output: Second static block
    • Static variable assignment executes:
      • j = 20 is assigned. j is now fully initialized and no longer in the RIWO state.
  • Step 3: Main Method Execution
    • The main method is finally invoked.
    • m1() is called. It prints the current value of j. Output: 20
    • System.out.println("Main method") runs. Output: Main method

--------------------------------------------------------------------------------

3. The "Illegal Forward Reference" and the State of RIWO

This strict ordering leads to a subtle but critical compiler rule regarding forward references. Sometimes you can access a static variable before its declaration, and other times you get a compile-time error. This behavior is governed by the variable's state and a compile-time safety feature.

To understand this, let's sharpen our definitions:

  • Read Indirectly Write Only (RIWO) State: The state of a static variable after the JVM has identified it (Step 1) but before its explicit value has been assigned (Step 2).
  • Direct Read: Accessing a variable within a static block before the line where it is declared and initialized.
  • Indirect Read: Accessing that same variable from a method that is called from the static block.

The rule that connects these concepts is simple but critical: if a variable is in the RIWO state, you cannot perform a Direct Read, but you can perform an Indirect Read.

The reason for this is a compiler safeguard. A direct read of a variable before its initializer has run is a likely logic error. The compiler can see this violation statically and fails with an "illegal forward reference" to prevent it. However, an indirect read through a method call is harder to analyze statically, so the check is effectively deferred to runtime. At runtime, the variable is guaranteed to have a value (its default), so the operation is permitted.

"if you are trying to perform Direct Read immediately compile error we are going to get saying illegal forward reference"

Let's see this in action.

Example 1: Illegal Forward Reference (Direct Read)

This code tries to print x directly in a static block that appears before its declaration.

class Test {
    static {
        System.out.println(x); // Direct Read
    }
    static int x = 10;
}

The compiler sees that x is in a RIWO state at the point of the direct read. This is a clear violation of the rule, and the code fails to compile with an "illegal forward reference" error.

Example 2: Successful Indirect Read

This code calls a method, m1(), from the static block. The m1() method then reads x.

class Test {
    static {
        m1(); // Indirect Read via method call
    }

    public static void m1() {
        System.out.println(x);
    }

    static int x = 10;
}

This code compiles and runs successfully. At runtime, m1() is called while x is in its RIWO state. Because this is an indirect read, it is permitted, and the program will print the default value of x, which is 0. After the static initialization is complete, the JVM then searches for the main method. Since it's not present, the program will terminate with a java.lang.NoSuchMethodError: main.

--------------------------------------------------------------------------------

Conclusion: Flow Over Fundamentals

The common "main is the entry point" rule is a simplification. The reality is a meticulous three-step sequence of identification, execution, and finally, the main method invocation. This sequence gives rise to the "Read Indirectly, Write Only" state, a temporary phase where variables exist with default values, leading to the counter-intuitive "illegal forward reference" error. Mastering this flow is the key to writing truly predictable, robust Java code, especially in applications with complex static initializers.

Now that you know main() isn't always the beginning, what other fundamental programming 'rules' are worth a second look?

Featured Post

How LLMs Really Work: The Power of Predicting One Word at a Time

  1.0 Introduction: The Intelligence Illusion The most profound misconception about modern AI is that it understands . While models like Cha...

Popular Posts