☕ Java

Thread Priority

Thread priority in Java is an integer hint to the OS scheduler indicating the relative importance of a thread compared to others. Java defines a scale from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10) with Thread.NORM_PRIORITY (5) as the default, and every thread inherits the priority of the thread that created it. The critical word in this definition is hint: thread priority is advisory, not mandatory. The JVM maps Java priorities to native OS thread priorities, and the OS scheduler uses those priorities according to its own scheduling policy, which varies by operating system, scheduler configuration, and system load. On some platforms, priority has a measurable effect on scheduling frequency; on others, it is almost entirely ignored. Priority must never be used as a correctness mechanism — any program that requires a thread to run before another for correctness, rather than merely preferring it, is broken and will fail on any platform where priorities are not honored. This entry covers the priority scale, inheritance, platform mapping, the correctness prohibition, starvation, priority inversion, and the narrow set of cases where priority hints are legitimately useful.

The Priority Scale, Inheritance, and Setting Priority

Java thread priorities are integers in the closed range [1, 10]. Three named constants in Thread define the landmarks: Thread.MIN_PRIORITY = 1 is the lowest possible priority, Thread.NORM_PRIORITY = 5 is the default for threads created from the main thread, and Thread.MAX_PRIORITY = 10 is the highest possible priority. Values outside [1, 10] are rejected by setPriority() with an IllegalArgumentException. Every thread inherits its initial priority from the thread that creates it. The main thread starts at NORM_PRIORITY (5). A thread created from the main thread with new Thread(...) starts at priority 5 unless explicitly changed. A thread created from a thread running at priority 8 starts at priority 8. This inheritance is the reason that thread pool implementations reset thread priorities to NORM_PRIORITY when creating worker threads — otherwise a worker thread created by a high-priority thread would run at high priority indefinitely, even when processing low-priority work. setPriority(int) changes the thread's priority and may be called before or after start(). Calling setPriority() after start() changes the underlying native thread's OS priority, subject to the platform mapping. getPriority() returns the current Java-level priority. Neither method is synchronized on the Thread object in a way that guarantees immediate visibility to the OS scheduler; the actual effect of a priority change is scheduler-dependent and may not be instantaneous. The ThreadGroup associated with a thread imposes a maximum priority ceiling: a thread's priority cannot exceed its ThreadGroup's maximum priority, even if setPriority() is called with a higher value. setMaxPriority() on a ThreadGroup lowers the ceiling for all new threads added to the group, but does not retroactively lower the priorities of existing threads in the group. Thread groups are a largely vestigial API in modern Java — their security and management functions have been superseded by ExecutorService and related abstractions — but their priority ceiling interacts with setPriority() in ways that can confuse developers who set priorities without checking the thread group.
Java
// ── Priority constants and defaults ──────────────────────────────────
System.out.println(Thread.MIN_PRIORITY);   // 1
System.out.println(Thread.NORM_PRIORITY);  // 5
System.out.println(Thread.MAX_PRIORITY);   // 10

// Main thread is NORM_PRIORITY:
System.out.println(Thread.currentThread().getPriority());  // 5

// ── Priority inheritance ──────────────────────────────────────────────
Thread parent = new Thread(() -> {
    Thread child = new Thread(() -> {
        // Inherits parent's priority:
        System.out.println("Child priority: " + Thread.currentThread().getPriority());  // 8
    }, "child");
    child.start();
    try { child.join(); } catch (InterruptedException e) {}
}, "parent");
parent.setPriority(8);
parent.start();
parent.join();

// ── Setting priority before and after start() ─────────────────────────
Thread t = new Thread(() -> {
    System.out.println("Running at priority: " + Thread.currentThread().getPriority());
}, "priority-demo");

// Before start — most reliable:
t.setPriority(Thread.MAX_PRIORITY);
System.out.println("Priority before start: " + t.getPriority());  // 10
t.start();
t.join();

// After start — still legal, effect is immediate but scheduler-dependent:
Thread running = new Thread(() -> {
    try { Thread.sleep(2000); } catch (InterruptedException e) {}
}, "adjustable");
running.start();
Thread.sleep(100);
running.setPriority(3);   // lower priority while running
System.out.println("Adjusted priority: " + running.getPriority());  // 3
running.interrupt();
running.join();

// ── IllegalArgumentException for out-of-range values ─────────────────
Thread bad = new Thread(() -> {}, "bad-priority");
try {
    bad.setPriority(0);    // below MIN_PRIORITY (1) — illegal
} catch (IllegalArgumentException e) {
    System.out.println("Priority 0 rejected: " + e.getMessage());
}
try {
    bad.setPriority(11);   // above MAX_PRIORITY (10) — illegal
} catch (IllegalArgumentException e) {
    System.out.println("Priority 11 rejected: " + e.getMessage());
}

// ── ThreadGroup priority ceiling ──────────────────────────────────────
ThreadGroup limitedGroup = new ThreadGroup("limited");
limitedGroup.setMaxPriority(6);   // ceiling at 6

Thread capped = new Thread(limitedGroup, () -> {
    // Requested 10, but group caps at 6:
    System.out.println("Actual priority: " + Thread.currentThread().getPriority());  // 6
}, "capped-thread");
capped.setPriority(Thread.MAX_PRIORITY);  // 10 requested
capped.start();
capped.join();

// ── Thread pool priority reset — why pools use NORM_PRIORITY ─────────
// ExecutorService worker threads inherit creator's priority then reset to NORM:
ExecutorService exec = Executors.newFixedThreadPool(2, r -> {
    Thread worker = new Thread(r, "pool-worker");
    worker.setPriority(Thread.NORM_PRIORITY);  // explicit reset — good practice
    return worker;
});

Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
exec.submit(() ->
    System.out.println("Worker priority: " + Thread.currentThread().getPriority())  // 5, not 10
);
exec.shutdown();
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);  // restore main thread

OS Mapping, Platform Behavior, and the Correctness Prohibition

Java thread priorities are mapped to native OS thread priorities by the JVM, and the mapping varies significantly by operating system. On Windows, the JVM maps Java priorities to Win32 thread priority levels: THREAD_PRIORITY_IDLE, THREAD_PRIORITY_LOWEST, THREAD_PRIORITY_BELOW_NORMAL, THREAD_PRIORITY_NORMAL, THREAD_PRIORITY_ABOVE_NORMAL, THREAD_PRIORITY_HIGHEST, and THREAD_PRIORITY_TIME_CRITICAL. Windows actively honors these priorities through its scheduler, and high-priority threads do receive preferential treatment when CPU time is scarce. On Windows, thread priority has a measurable and significant effect. On Linux, the situation is entirely different. The default Linux scheduler (CFS, Completely Fair Scheduler) does not map Java thread priorities to any meaningful scheduling parameter for most JVM implementations. The JVM on Linux may use pthread_setschedparam() to set scheduler policy and priority, but the default SCHED_OTHER policy ignores priority values — CFS determines scheduling based on virtual runtime, not priority. Java priorities may be mapped to nice values (-20 to +19), but this mapping is implementation-specific and requires CAP_SYS_NICE capability to raise priority (lower nice value), which most JVMs running as unprivileged users do not have. The practical result: on most Linux deployments, Java thread priority has no observable effect on scheduling. Programs that depend on thread priority for correctness will work on Windows in development and silently break on Linux in production. macOS uses a hybrid scheduler (similar to BSD's ULE scheduler) that does honor thread priorities to a degree, placing threads in different scheduling bands. GCD (Grand Central Dispatch) interactions and QoS classes complicate the picture further. In general, macOS behavior is somewhere between Windows (fully honors) and Linux (mostly ignores). The correctness prohibition is absolute: no Java program should require a specific execution ordering between threads for correctness and rely on thread priority to deliver that ordering. If thread A must observe a write by thread B before proceeding, the correct mechanism is a synchronization primitive: a lock, a volatile variable, a CountDownLatch, a Semaphore, a BlockingQueue, or any other construct that establishes a happens-before relationship. Thread priority establishes no happens-before relationship and provides no memory visibility guarantees. A program that coincidentally works because high-priority threads happen to run before low-priority threads on a specific OS will break on a different OS, a different JVM version, a different machine load profile, or after a JVM update changes the priority mapping.
Java
// ── Platform mapping — what Java priorities become at the OS level ───
// HotSpot JVM on Linux (typical mapping to nice values, requires privileges):
// Java 1  → nice +19  (lowest)
// Java 5  → nice  0   (normal)
// Java 10 → nice -20  (highest, requires CAP_SYS_NICE — rarely available)

// HotSpot JVM on Windows (Win32 thread priority):
// Java 1  → THREAD_PRIORITY_IDLE        (-15)
// Java 2  → THREAD_PRIORITY_LOWEST      (-2)
// Java 4  → THREAD_PRIORITY_BELOW_NORMAL(-1)
// Java 5  → THREAD_PRIORITY_NORMAL      (0)
// Java 6  → THREAD_PRIORITY_ABOVE_NORMAL(+1)
// Java 9  → THREAD_PRIORITY_HIGHEST     (+2)
// Java 10 → THREAD_PRIORITY_TIME_CRITICAL(+15)

// ── Priority has NO effect on correctness — ever ──────────────────────

// WRONG: relying on priority for execution ordering (broken on Linux):
int[] sharedData = new int[1];

Thread writer = new Thread(() -> {
    sharedData[0] = 42;
    System.out.println("Writer: wrote 42");
}, "writer");

Thread reader = new Thread(() -> {
    // BROKEN ASSUMPTION: high priority means reader sees writer's data
    System.out.println("Reader: read " + sharedData[0]);  // may print 0 on Linux
}, "reader");

writer.setPriority(Thread.MAX_PRIORITY);   // doesn't establish happens-before
reader.setPriority(Thread.MIN_PRIORITY);
writer.start();
reader.start();
writer.join(); reader.join();
// Works sometimes on Windows; broken on Linux; broken under any load

// CORRECT: use synchronization to establish happens-before:
CountDownLatch writerDone = new CountDownLatch(1);
int[] safeData = new int[1];

Thread safeWriter = new Thread(() -> {
    safeData[0] = 42;
    writerDone.countDown();  // happens-before: latch release → latch await
}, "safe-writer");

Thread safeReader = new Thread(() -> {
    try {
        writerDone.await();   // waits for happens-before to be established
        System.out.println("Reader: read " + safeData[0]);  // always 42
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}, "safe-reader");

safeWriter.start(); safeReader.start();
safeWriter.join();  safeReader.join();

// ── Verifying that priority has no effect in a specific environment ───
Thread high = new Thread(() -> {
    long count = 0;
    long end = System.currentTimeMillis() + 2000;
    while (System.currentTimeMillis() < end) count++;
    System.out.println("HIGH priority counted: " + count);
}, "high-priority");

Thread low = new Thread(() -> {
    long count = 0;
    long end = System.currentTimeMillis() + 2000;
    while (System.currentTimeMillis() < end) count++;
    System.out.println("LOW priority counted: " + count);
}, "low-priority");

high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
high.start(); low.start();
high.join();  low.join();
// On Linux: counts roughly equal (priority ignored by CFS)
// On Windows: high may count significantly more than low

Starvation, Priority Inversion, and Legitimate Uses of Priority

Starvation occurs when a low-priority thread never gets CPU time because high-priority threads continuously consume all available CPU cycles. The theoretical risk is real: on scheduling algorithms that strictly honor priorities (Windows preemptive priority scheduling), a set of high-priority CPU-bound threads can starve all lower-priority threads indefinitely. Modern OS schedulers incorporate starvation prevention mechanisms — Windows gradually boosts the priority of starved threads ("priority boosting"), and Linux CFS uses virtual runtime accounting that naturally prevents indefinite starvation. In practice, starvation from Java thread priorities is rare unless the system has more high-priority CPU-bound threads than CPU cores. It becomes a real risk in low-latency or real-time systems where threads are intentionally set to high priority and perform long CPU-bound operations. Priority inversion is a subtler and more dangerous problem that occurs when a low-priority thread holds a resource (a lock) needed by a high-priority thread. The high-priority thread cannot proceed because it is BLOCKED waiting for the lock. A medium-priority thread, needing no locks and running at higher priority than the lock holder, runs instead. The net effect: a medium-priority thread runs while a high-priority thread is blocked, with the low-priority lock holder unable to run to release the lock. The apparent priority ordering is inverted. Priority inheritance protocols (where a lock holder temporarily inherits the priority of the highest-priority thread waiting for its lock) solve priority inversion, but Java's synchronized and ReentrantLock do not implement priority inheritance. This is one reason that real-time Java systems (RTSJ) require specialized runtime environments. The legitimate use cases for thread priority in production Java are narrow. Background tasks that should not compete with user-facing work — garbage collection helpers, analytics batch jobs, cache warmers, index rebuilders, telemetry emitters — can run at below-normal priority (3–4) to reduce their impact on latency-sensitive threads running at normal priority (5). This is a soft optimization, not a hard guarantee: the background threads may still run frequently enough to complete in a reasonable time, and the user-facing threads get preferential treatment when CPU is scarce. The effect is measurable on Windows and in CPU-saturated scenarios on Linux with privileged nice values. It is never a substitute for proper capacity planning or work scheduling.
Java
// ── Starvation demonstration (theoretical — scheduler-dependent) ──────
// 8 high-priority CPU-spinners starving 1 low-priority thread:
AtomicBoolean running = new AtomicBoolean(true);
AtomicLong lowCount  = new AtomicLong(0);
AtomicLong highCount = new AtomicLong(0);

// 1 low-priority thread:
Thread low = new Thread(() -> {
    while (running.get()) lowCount.incrementAndGet();
}, "starved-low");
low.setPriority(Thread.MIN_PRIORITY);

// 8 high-priority CPU spinners:
Thread[] highs = new Thread[8];
for (int i = 0; i < highs.length; i++) {
    highs[i] = new Thread(() -> {
        while (running.get()) highCount.incrementAndGet();
    }, "high-" + i);
    highs[i].setPriority(Thread.MAX_PRIORITY);
}

low.start();
for (Thread h : highs) h.start();
Thread.sleep(3000);
running.set(false);
for (Thread h : highs) h.join();
low.join();

System.out.printf("High total: %,d%n", highCount.get());
System.out.printf("Low  total: %,d%n", lowCount.get());
// On Windows: low may get nearly 0 iterations — starvation
// On Linux with CFS: low gets a fair share regardless of priority

// ── Priority inversion scenario ───────────────────────────────────────
ReentrantLock sharedResource = new ReentrantLock();

// LOW priority acquires the lock:
Thread lowPrio = new Thread(() -> {
    sharedResource.lock();
    try {
        System.out.println("LOW: acquired lock, doing slow work");
        Thread.sleep(2000);   // holds lock for 2 seconds — simulates slow work
        System.out.println("LOW: releasing lock");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        sharedResource.unlock();
    }
}, "low-prio");
lowPrio.setPriority(Thread.MIN_PRIORITY);

// HIGH priority needs the lock — will be BLOCKED on lowPrio:
Thread highPrio = new Thread(() -> {
    System.out.println("HIGH: waiting for lock...");
    sharedResource.lock();
    try {
        System.out.println("HIGH: finally got the lock");
    } finally {
        sharedResource.unlock();
    }
}, "high-prio");
highPrio.setPriority(Thread.MAX_PRIORITY);

// MEDIUM priority does CPU work — may preempt lowPrio, blocking highPrio:
Thread mediumPrio = new Thread(() -> {
    long count = 0;
    long end = System.currentTimeMillis() + 3000;
    while (System.currentTimeMillis() < end) count++;
    System.out.println("MEDIUM: completed CPU work, count=" + count);
}, "medium-prio");
mediumPrio.setPriority(6);

lowPrio.start();
Thread.sleep(50);   // ensure lowPrio has the lock
highPrio.start();
mediumPrio.start();
highPrio.join(); mediumPrio.join(); lowPrio.join();
// Effective order: medium runs, high waits, low is delayed — priority INVERTED

// ── Legitimate use: background tasks at reduced priority ──────────────
ExecutorService foreground = Executors.newFixedThreadPool(4, r -> {
    Thread t = new Thread(r, "fg-worker");
    t.setPriority(Thread.NORM_PRIORITY);     // 5 — user-facing work
    return t;
});

ExecutorService background = Executors.newFixedThreadPool(2, r -> {
    Thread t = new Thread(r, "bg-worker");
    t.setPriority(Thread.MIN_PRIORITY + 1);  // 2 — background analytics, reindexing
    t.setDaemon(true);                       // don't block JVM shutdown
    return t;
});

// Foreground tasks get CPU preference when system is saturated:
foreground.submit(() -> processUserRequest());    // priority 5
background.submit(() -> rebuildSearchIndex());    // priority 2 — yields to foreground

// ── Summary: when priority adjustment is appropriate ──────────────────
// DO use reduced priority (24) for:
//   - Background analytics and reporting jobs
//   - Cache warming and precomputation
//   - Log flushing and telemetry emission
//   - Search index maintenance
//   - Speculative prefetching

// DO NOT use priority for:
//   - Ensuring thread A runs before thread B (use CountDownLatch, Semaphore, etc.)
//   - Making a thread "more responsive" in latency-critical paths (reduce contention instead)
//   - Replacing proper work scheduling and capacity planning
//   - Any correctness guarantee across platforms

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.