David Dice's Weblog
Java Thread Priority Revisited in JDK7
In an earlier blog entry I described how Java thread priorities relate to native thread priority. We recently ran into bug 6518490 that forced me to change the default mapping of priorities on Solaris™ as previously described.
Briefly, in the Solaris IA(interactive) or TS(timeshare) scheduling classes, the effective priority of a thread is function of the thread's assigned priority and a thread-specific system-managed behavioral factor. (That's a gross oversimplification. If you're curious, I recommend Solaris Internals, 2nd Edition Section 3.7 followed by a skim of ts.c. In the case of Java the JVM maps the logical Java priority set via Thread.setPriority() to an TS- or IA-specific priority, and then calls priocntl() to set the assigned priority for the thread. The scheduler avoids indefinite starvation of low priority threads by periodically applying a boost (this is the system-managed behavioral factor) for threads that have languished too long on the ready queue without having been run. In addition, threads that block voluntarily -- as opposed to exhausting their time slice and requiring involuntary preemption -- may receive a boost to their effective priority by way of changes to the previously mentioned behavioral factor.
Lets say we have a 2x multiprocessor system, otherwise idle, with threads T1 and T2 at high assigned priority and T3 at low assigned priority. T1 and T2 loop, yielding, or alternatively they might pass a "token" back and forth via a pipe. Critically, they block often, so they receive the aforementioned boost to their effective priorities. Thread T3 simply loops, computing the digits of π. T3 will languish on the ready queue. Over time the scheduler will boost T3's effective priority, but unfortunately that boost isn't sufficient to push T3's priority beyond that of the boosted effective priorities of T1 and T2, so T3 can starve indefinitely.
To avoid the problem I've inverted the default for the -XX:[+/-]UseThreadPriorities flag from True to False on Solaris. The defaults setting for the flag remains unchanged on other platforms. Running all Java threads at the same priority sidesteps the problem and avoid starvation. I considered just posting a work-around and leaving the flag polarity unchanged, but the bug is difficult to diagnose properly. A work-around, albeit simple, is useless if a customer is forced to diagnose and identify a rather opaque and exotic bug. Put another way, a work-around is no better than the diagnostic procedure used to identify the associated malady. The change to UseThreadPriorities is in JDK7 and is propagating back through JDK6 and JDK5. If you desperately want Java threads priorities to map to underlying native priority you can use -XX:+UseThreadPriorities, but in a sense you'll be accepting the risk of the hang. You have to explicitly opt-in to enable priority mapping. Priorities are a quality-of-implementation concern, not a correctness concern. Recall that thread priorities are strictly advisory, and the JVM has license and latitude to implement or change them as needed. They should never be depended upon to ensure progress (relative to other threads) or lack of progress (i.e., synchronization).
Finally, this changed isn't as significant as it might seem. Under IA and TS, the upper half of the logical priority range was previously mapped to native "high" and the lower half was mapped over the range of native priorities. The only observable difference between the old behavior and running with thread priorities disabled is that Java threads assigned priorities in the lower half of the logical range will now run at the same effective (native) priority as threads in the upper half. When competing for CPU cycles with other threads, these low priority threads will receive relatively more cycles from the scheduler than they did in the past.
Posted at 05:08PM Jan 10, 2008 by David Dice in General | Comments[4]
So basically, you're working around a thread-starvation bug in Solaris. That sounds weird. I would have thought that Sun would have fixed something like that a long time ago.
An alternate work-around might be to map the logical Java thread priorities to 3 or 4 Sun thread priorities, with all of the Sun thread priorities at one end of the Sun priority range.
Having small gaps would make it less likely that you'd hit the problem induced by the sun "behavioural" adjustment.
Posted by Noel Grandin on January 11, 2008 at 06:13 AM EST #
Hi Noel, Yes - arguably the JVM change is a work-around for a kernel issue and ideally I'd prefer to have seen this addressed in the kernel. The anti-starvation policy in the kernel scheduler is a QoI optimization, but provides no guarantees of eventual progress. But with a work-around in the JVM we can provide relief to the customers in the field in a timely manner. The conditions where the hang will manifest are rather exotic and contrived, but we've seem it with increasing frequency.
I experimented with restricting priority bands as you'd mentioned, but the bug still recurs. In my judgement it was best to simply eliminate the hang instead of decreasing the frequency.
Regards, -Dave
Posted by Dave Dice on January 11, 2008 at 09:39 AM EST #
Hi Dave, it sounds like the 'effective boosting' algorithm that determines which Thread gets preference is incorrect. If it is indeed counting the number of actual cycles that each thread gets then eventually the lower priority thread will get some cycles on some core, irrespective of which cores or how many cores are running the other (higher priority) threads. If the logic somehow became incorrect when the VM threads are running on multiple cores (or processors) then what is this effective boosting algorithm actually counting to determine which thread gets to go?
Posted by Matt on January 28, 2008 at 02:45 AM EST #
Hi Matt, Folks on the Solaris team indicated that the anti-starvation facility -- which provides transient boosts to threads that have languished for long periods on the dispatch queues -- doesn't absolutely guarantee eventual progress (non-starvation) in the presence of mixed assigned priorities. In our case we have some threads at high(er) assigned priority that deschedule themselves voluntarily and frequently, and are thus beneficiaries of a behavioral "bonus". These threads are competing with threads at a lower assigned priority. The anti-starvation adjustment isn't sufficient to push the effective priority of this 2nd group of threads above that of the 1st.
With respect to CPU cycle accounting, older kernels used the 100Hz "tick" interrupt and were vulnerable to quantization problems but that's changing with the arrival of cycle-accurate accounting.
Also, under Solaris each CPU has its own dispatch queue and makes local decisions but the policies and parameters are such that these local decisions collectively achieve the desired global scheduling policy. There is no centralized scheduler. As a consequence of this design it's possible encounter situations where CPU#0 is running thread T5 and T5's effective priority is less than that of thread T6, which resides on CPU#1's dispatch queue. (You can avoid that scenario by placing threads in the real-time "RT" scheduling class).
Posted by Dave Dice on January 28, 2008 at 10:47 AM EST #