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
// ── 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 threadOS Mapping, Platform Behavior, and the Correctness Prohibition
// ── 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 lowStarvation, Priority Inversion, and Legitimate Uses of Priority
// ── 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 (2–4) 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