Priority Inversion
A high-priority thread waits on a lock held by a low-priority thread. A medium-priority thread runs, preempting the low-priority holder, which means the high-priority thread also waits indirectly. The fix is priority inheritance: bump the holder's priority to match the highest waiter.
The setup in plain English
Three threads, ranked by urgency:
- High is critical. Should run as soon as possible.
- Medium is regular work. Will preempt anything below it.
- Low is background. Lowest urgency.
One shared lock. Low takes the lock to do something quick. Then High needs the same lock and has to wait. So far this is normal: High will get the lock as soon as Low finishes.
Then Medium becomes runnable. The OS sees Medium is more important than Low and switches CPU to Medium. Low is now paused, still holding the lock. Medium runs and runs (it doesn't need the lock at all). High is stuck behind Low. Low is stuck behind Medium.
End result: High, the most critical thread, is blocked by Medium, which it should outrank. The order has inverted.
A timeline
Three threads, one lock held by Low, no priority inheritance. Time flows top to bottom.
High wanted to outrank Medium. Medium wanted to outrank Low. But Low was inside the critical section, and Medium doesn't care about the lock. So Medium runs ahead of High. Inversion.
The famous example
Mars Pathfinder, July 1997. The rover landed and started rebooting on its own after a few days. The cause was exactly this pattern:
- Low: a weather-data thread held a lock on the information bus.
- High: a bus management thread waited for that lock.
- Medium: a comms thread became runnable and preempted the weather thread.
- The bus management thread missed its watchdog deadline. The watchdog rebooted the rover.
NASA fixed it by flipping a flag on the mutex (enable priority inheritance) and pushed the patch to Mars over a slow link.
The fix: priority inheritance
When a high-priority thread starts waiting on a lock held by a lower-priority thread, the OS temporarily promotes the holder to the waiter's priority. The holder finishes, releases the lock, and drops back to its original priority.
Without priority inheritance (the broken case again, summarised):
With priority inheritance (the fix):
POSIX exposes this as PTHREAD_PRIO_INHERIT on the mutex attribute. Linux's PI futex implements it. Real-time operating systems (VxWorks, FreeRTOS, RT Linux) support it natively. Java does not. Go does not (no thread priorities). Python does not.
When this matters in normal code
Rarely. The classic setup needs three things together: real thread priorities, an OS scheduler that respects them strictly, and locks shared across priority levels. Most server code has none of those, so the textbook bug doesn't appear.
The same shape without priorities Even without strict thread priorities, the same pattern appears: a fast operation stuck behind a slow one because they share a lock. A user request waiting on a logging path that hit a slow disk. A real-time audio thread waiting on the UI thread mid-GC. The fix generalises: don't share locks between things that need to scale independently. Critical work should have its own queue, its own pool, its own lock, or no lock at all.
The lesson worth keeping is more general than the bug. When workloads of different urgency share the same lock or queue, the slowest one sets the floor for everyone else.
Implementations
A low-priority logger holds a lock. A high-priority handler needs it. A medium-priority background task starts running and preempts the logger. The handler stalls until medium yields.
1 Object lock = new Object();
2
3 Thread low = new Thread(() -> {
4 synchronized (lock) {
5 writeLog(); // long, low-priority
6 }
7 }, "low");
8 low.setPriority(Thread.MIN_PRIORITY);
9
10 Thread medium = new Thread(() -> {
11 while (true) crunchNumbers(); // CPU-bound
12 }, "medium");
13 medium.setPriority(Thread.NORM_PRIORITY);
14
15 Thread high = new Thread(() -> {
16 synchronized (lock) { // blocked behind low
17 handleEmergency();
18 }
19 }, "high");
20 high.setPriority(Thread.MAX_PRIORITY);The JVM does not guarantee strict priority scheduling on most platforms. The pragmatic fix is to design without thread priorities at all. For a critical request, give it a dedicated thread, a dedicated executor, or a separate process so it does not share locks with low-priority work.
1 // Critical work on its own dedicated executor
2 ExecutorService critical = Executors.newFixedThreadPool(4,
3 r -> { Thread t = new Thread(r, "critical"); t.setDaemon(false); return t; });
4
5 // Background work on a separate executor with its own resources
6 ExecutorService background = Executors.newFixedThreadPool(2,
7 r -> { Thread t = new Thread(r, "background"); t.setDaemon(true); return t; });
8
9 // No shared lock between them, no inversion possible.Key points
- •Three threads, one lock. High waits on Low for the lock. Medium runs, preempting Low. High is blocked indefinitely.
- •Famous case: Mars Pathfinder, 1997. The rover kept rebooting due to a priority inversion in the VxWorks scheduler.
- •Priority inheritance: while Low holds the lock, raise its priority to High's so Medium cannot preempt.
- •Common in real-time systems, audio drivers, embedded code, kernel synchronisation. Less common in Java/Go/Python.
- •JVM and most modern OS schedulers do not implement priority inheritance by default; problem solved by avoiding strict thread priorities in the first place.