When many
threads are runnable, the thread scheduler determines which ones get to run,
and for how long. Any reasonable operating system will try to make this
determination fairly, but the policy can vary. Therefore, well-written programs
shouldn’t depend on the details of this policy. Any program that
relies on the thread scheduler for correctness or performance is likely to be
nonportable.
The best way
to write a robust, responsive, portable program is to ensure that the average
number of runnable threads is not significantly greater than the
number of processors. This leaves the thread scheduler with little choice: it
simply runs the runnable threads till they’re no longer runnable. The program’s
behavior doesn’t vary too much, even under radically different
thread-scheduling policies. Note that the number of runnable threads isn’t the
same as the total number of threads, which can be much higher. Threads that are
waiting are not runnable.
The main
technique for keeping the number of runnable threads down is to have each
thread do some useful work and then wait for more. Threads should
not run if they aren’t doing useful work.
Threads should
not busy-wait, repeatedly
checking a shared object waiting for something to happen. Besides making the
program vulnerable to the vagaries of the scheduler, busy-waiting greatly
increases the load on the processor, reducing the amount of useful work that
others can accomplish. As an extreme example of what not to do,
consider this perverse reimplementation of CountDownLatch:
// Awful CountDownLatch implementation - busy-waits
incessantly!
public class SlowCountDownLatch {
private int count;
public SlowCountDownLatch(int count) {
if (count < 0)
throw new IllegalArgumentException(count + " <
0");
this.count = count;
}
public void await() {
while (true) {
synchronized(this) {
if (count == 0) return;
}
}
}
public synchronized void countDown() {
if (count != 0)
count--;
}
}
On my machine,
SlowCountDownLatch is about 2,000
times slower than CountDownLatch when 1,000
threads wait on a latch. While this example may seem a bit far-fetched, it’s
not uncommon to see systems with one or more threads that are unnecessarily
runnable. The results may not be as dramatic as Slow-
CountDownLatch, but performance and portability are likely to suffer.
When faced
with a program that barely works because some threads aren’t getting enough CPU
time relative to others, resist the temptation to “fix” the program by
putting in calls to Thread.yield. You may
succeed in getting the program to work after a fashion, but it will not be
portable. The same yield invocations
that improve performance on one JVM implementation might make it worse on a
second and have no effect on a third. Thread.yield
has
no testable semantics.
A related
technique, to which similar caveats apply, is adjusting thread priorities. Thread
priorities are among the least portable features of the Java platform. It is not
unreasonable to tune the responsiveness of an application by tweaking a few
thread priorities, but it is rarely necessary and is not portable.
In summary, do
not depend on the thread scheduler for the correctness of your program. The
resulting program will be neither robust nor portable. As a corollary, do not
rely on Thread.yield or thread
priorities. These facilities are merely hints to the scheduler. Thread
priorities may be used sparingly to improve the quality of service of an
already working program, but they should never be used to “fix” a program that
barely works.
Reference: Effective Java 2nd Edition by Joshua Bloch