The threads API of the JVM provides very limited control over the scheduling of the threads themselves, and this control is not guaranteed, but it is JVM implementation dependent. In order to better understand the way different implementations do thread scheduling, we devised a simple test application that within the constraints of the API itself tries to gather some information about the way the underlying JVM is handling scheduling of threads.
The test application is composed of two classes:
TestThread: Extends Threads and its run method consist of a while look that increments a counter while the thread has not been interrupted. This is thus a CPU bound process, with no IO operations, so all context switches are due exclusively to the thread scheduling mechanism. The main loop where the thread executes is the fragment:
tbegin = System.nanoTime();
while(!interrupted()) {
counter++;
if (counter/SAMPLERATE<t.length && counter%SAMPLERATE==0)
t[counter/SAMPLERATE]=System.nanoTime()-tbegin;
}
The thread takes samples of the elapsed time every SAMPLERATE counter increments, so this will allow us see when the thread was really in execution and when it was idle as will be shown below in the results section. Important to notice the use of System.nanoTime(), which (we believe) uses a processor timer and not an operating system call.
VMThreads: This class setups the environment for the test, composed of a set of TestThreads, one per priority level (except for the maximum priority, which is reserved for the main thread). It then starts the threads, sleeps for some given amount of time, and interrupts the TestThreads. After this, the main thread collects the statistics from the TestThreads that will allow us to identify some of the characteristics of the underlying thread scheduler and saves them to a file.
The source code can be found here.
The test was run on two systems:
A P4 system with linux kernel 2.6.9-34.EL, glibc-2.3.4 and SUN's JVM 1.5.0_06
An Athlon64 system with Windows XP Media Edition and SUN's JVM 1.5.0_06
The test length was set to 30 seconds to give chances of recording some number of contexts switches and the system activity was keep to a minimum in order to avoid as much as possible the noise introduced by other processes scheduled by the operating system.
Linux system:

Windows system:

A first general observation is that in both cases, the priority assigned to the threads did not really have a significant effect on the scheduling order. In both cases, threads with lower priority reached larger counts over the recorded time. The only clear exception was in the windows system for the thread with maximum priority ( =8 ), which kept running as far as samples were collected.
It is also noticeable, a clear distinction of active periods (with positive slope) and idle periods (flat curve, meaning the counter did not incremented during this time) in both cases, although the particular duration of the active/inactive periods is clearly OS dependent.
In the case of the windows system, there are some anomalies with the time values, due to the fact that the fuction nanoTime() did not return always increasing values.
The next couple of figures show the zooming of the previous ones, to give some more detail about the schedule:

For the linux case, the time a thread is active is constant and it is approximately 0.1 seconds. The idle periods are 0.8 seconds. Clearly the time-slot of different threads overlap. The figure also illustrates two scheduling periods, showing exactly the same sequence of thread activations, signaling a round-robin schedule, that looking back at the original picture, holds all the time except during the startup phase.

In the windows case, zooming on a period where most of the threads were active and not estrange measurements appear, it is interesting to observe that there is not overlap between threads, which makes us believe the JVM threads are actual OS threads. In many cases also, the termination and the beginning periods of consecutive threads coincide, reinforcing the previous conclusion. The active period of a thread is constant and is 0.19 seconds.
A nice suggestion we received was to modify the experiment to begin taking samples after some stabilization delay. The modification to the code is trivial and the results in the Linux case are shown in the following figures:


The interesting observation of the first graph is that threads with priority 2 and 3 achieved a slight larger rate of progress, having low priorities. The second one shows three scheduling cycles, where the same round-robin policy is observed with quantum of 0.1 seconds.