Threads
Multithreaded applications extend the idea of multitasked processes by
taking them a level lower: individual programs now appear to do
multiple tasks at the same time. The essential difference is that
while each process has a complete set of its own variables, threads
share the same data segment. It takes much less overhead to create and
destroy threads than it does to launch new processes; and cross-process
communication (even when permitted) is much slower than cross-thread
communication.
A thread consists of three main parts: the virtual CPU, the code this
CPU is executing, and the data that the code is working on. In Java,
the virtual CPU functionality (and complexity) is encapsulated in the
Thread class. The code and the data are architected in
the application. It is important to note that these three dimensions
are effectively independent. A thread can execute code that is the
same as another thread's code, or it can execute different code. A
thread can have access to the same data or different data.
Thread states
Threads are controlled by triggering state transitions. Thread states are:
- ready (not waiting for anything except the CPU)
- running (thread is actually performing work)
- various dormant states (suspended, asleep, blocked,
waiting)
- dead (all done, cannot run again)
A thread is "put into play" by calling its start() method. This
triggers a transition to the ready state.
Every thread has a priority. The priority is an integer from 1 to 10. The
default priority is 5, but all newly created threads are assigned the
priority of their creating thread. getPriority() and
setPriority() can be used to query and manipulate the priority.
The specifics of how thread priorities affect scheduling are platform
dependent. The Java specification states that threads must have priorities,
but it does not dictate precisely what the scheduler should do about
priorities. This vagueness is a problem - algorithms that rely on
manipulating thread priorities are unpredictable. [Roberts, p198]
Generally however, all threads in the ready state are placed in the
FIFO queue corresponding to their priority, and the scheduler moves the
first thread on the highest priority queue to the running state.
[Sun275, p13-7]
On Solaris, the thread scheduler is "preemptive".
A thread runs until it either ceases to be runnable, or another thread of
higher priority becomes runnable. In the latter case, the current thread
is "preempted" by the thread of higher priority. On Windows, the thread
scheduler is
"time-sliced". A thread is only allowed to execute for a limited amount
of time. It is then moved to the ready state, where it must
contend with all other runnable threads. [Roberts, p205]
Given that Java code needs to "run everywhere", applications should not
assume "time-sliced" scheduling, and must ensure that other threads are
given a chance to execute. The next several sections discuss "the art
of juggling many simultaneous threads".
Yielding
yield() will allow waiting threads of equal or higher priority
to execute. If there are no such threads, the current thread does not
stop executing. yield() is a static method of class
Thread. It always acts upon the current thread (you don't
have to have an object reference in order to call it).
Suspending
A thread that receives a
suspend() message enters the suspended state until it
receives a resume() message or a
stop() message. A thread can be suspended by itself or another
thread, but it can only be resumed by another thread. Unlike
yield(), both suspend() and
resume() are non-static methods.
Sleeping
sleep() is a static method that causes the current thread to
become dormant for the specified period of milliseconds (at a minimum).
After the time-out value expires (or the thread is explicitly sent an
interrupt() message), the thread is moved back to the
ready state, and begins running again whenever the scheduler
decides.
Note that sleep() and
yield() are both static. They operate on the currently
running thread. Also note that a call to
sleep() allows threads of lower priority a chance
to execute, and the
yield() method only gives threads of the same or higher
priority a chance.
Blocking
All Java I/O methods cause the invoking thread to surrender the CPU if
the request cannot be immediately fulfilled. When the interaction with
the outside world completes, the thread transitions back to the
ready state.
The
join() method causes the current thread to wait until the
thread on which join() is called terminates. This feature
can be used to delay one thread until a "timer" thread completes.
Waiting
In the diagram, the waiting state
is separated from the other "dormant" states to emphasize that it is
very different. The
wait() method causes a running thread to surrender the
CPU. notify() and
notifyAll() cause waiting threads to return to the
ready state. These methods are not implemented in the class
Thread but in the class
Object. The use of these methods is very subtle, and is
discussed in a later section.