<!-- --><!-- --><!-- --><!-- --><!-- -->
<!-- -->
by Eric Giguere
February 2003
?
One of the defining features of the Java environment is its built-in support for threads
. Threads let an application perform multiple activities simultaneously. When used properly, threads let the application's user interface remain responsive while it performs lengthy operations like network communication or very complicated computations. While user interface responsiveness is important no matter what the platform, it's even more so on the handheld and consumer-oriented devices that the J2ME platform targets. A basic understanding of how to use threads is key to writing effective J2ME applications, whether you're using the more limited facilities of the Connected Limited Device Configuration (CLDC) or the fuller features of the Connected Device Configuration (CDC). This article explains the concepts behind threads and how you can use them in your own applications.
<!-- -->
?
What is a Thread?
The Java Tutorial
defines a thread as "a single sequential flow of control within a program." Threads are the fundamental units of program execution. Every running application has at least one thread. An application consisting of two or more threads is known as a multithreaded
application.
?
Each thread has a context
associated with it. The context holds information about the thread, such as the address of the currently executing instruction and storage for local variables. The context is updated as the thread executes. The context also stores the thread's state. A thread can be in any of the following states:
- A running
thread is executing code.
- A ready
thread is ready to execute code.
- A suspended
thread is waiting on an external event. For example, it may be waiting for data to arrive from another device. After the event occurs, the thread transitions back to the ready state.
- A terminated
thread has finished executing code.
A thread that is running, ready, or suspended is a live
thread. A terminated thread is also known as a dead
thread.
?
Although an application may have many threads, the underlying device has a small, fixed number of processors (often just one or two) available for code execution. Threads share these processors by taking turns being in the running state. This arrangement is called thread scheduling
.
?
Thread scheduling is a complicated affair over which the application has little control. The system (either the underlying operating system or the Java virtual machine, depending on how the threads are implemented) gives each ready thread a chance to run for a short time (the suspended threads are ignored), switching rapidly from one thread to another. This context switching
can occur at any time. The only real influence the application has over thread scheduling is via the thread's priority level
: higher-priority threads execute more often than those with lower priority.
<!-- -->
?
The Thread Object
Threads are dynamic in nature, existing only as part of a running application. The Java runtime associates each live thread with an instance of java.lang.Thread
. This class is used to start new threads and to get and set thread priorities. Your code can obtain access to the current thread's instance of Thread
at any time by invoking Thread.currentThread()
.
?
J2ME provides two variants of the Thread
class, both subsets of the J2SE version 1.3. CLDC's Thread
class includes only these methods:
-
activeCount()
-
currentThread()
-
getPriority()
-
isAlive()
-
join()
-
run()
-
setPriority()
-
sleep()
-
start()
-
yield()
Notice the lack of methods to stop or interrupt a running thread - more on this later.
CDC's Thread
class, on the other hand, supports the full J2SE functionality, including support for thread groups
(groups of threads that can be manipulated as a set) and daemon threads
(an application automatically terminates when all non-daemon threads have terminated, even if one or more daemon threads are still active). The CDC Thread
class is not identical to the J2SE version, however, because it does not include deprecated methods, including those that stop or interrupt threads.
<!-- -->
?
Starting Threads
To start a new thread you need two things: an instance of java.lang.Thread
and an object that implements the java.lang.Runnable
interface.
The Runnable
interface defines a run()
method with no arguments and no return value. Here's a simple implementation:
<!-- -->
public class DoSomething implements Runnable {
public void run(){
// here is where you do something
}
}
|
<!-- -->
You create a Runnable
object, use it to create a new Thread
instance, then invoke the latter's start()
method:
<!-- -->
...
DoSomething doIt = new DoSomething();
Thread myThread = new Thread( doIt );
myThread.start();
...
|
<!-- -->
The system starts a new thread to invoke the Runnable's run()
method. When the thread returns from the run()
method, the system terminates the thread.
?
As it happens, the Thread
class itself implements the Runnable
interface, so another way to accomplish the same thing is to create a subclass of Thread
that overrides the run()
method, as in this example:
<!-- -->
public class DoAnotherThing extends Thread {
public void run(){
// here is where you do something
}
}
|
<!-- -->
You start the thread by creating an instance of the subclass and invoking its start()
method:
<!-- -->
...
DoAnotherThing doIt = new DoAnotherThing();
doIt.start();
...
|
<!-- -->
Which way is preferable? Functionally, there is no real difference. If you can implement the Runnable
interface on a class that also serves other purposes, however, you avoid the overhead of defining an entirely new class. That extra overhead is minimal, of course, but in very constrained environments (some mobile phones limit J2ME applications to 30K) every little bit helps.
?
Furthermore, inheriting from Thread
is actually more limiting, because the start()
method of any given Thread
object can be invoked only once.
?
There are no restrictions on what a thread does in its run()
method. A thread can perform a single action and immediately exit from run()
. More commonly it repeats some action until some condition is satisfied:
<!-- -->
public void run(){
boolean quit = false;
while( !quit ){
// repeat some action
quit = .... // decide if it should quit
}
}
|
<!-- -->
Note that a thread has access to all the methods and fields of the Runnable
object used to create the thread. Because the run()
method has no parameters, passing parameters to the start()
method is the only way to initialize a thread. Here's a simple example:
<!-- -->
public class ThreadedDoIt implements Runnable {
private SomeClass state;
public ThreadedDoIt( SomeClass initState ){
state = initState;
new Thread( this ).start();
}
public void run(){
// do something based on "state" field
}
}
|
<!-- -->
If two threads simultaneously access the same data, thread synchronization (discussed later) becomes a concern.
<!-- -->
?
Stopping Threads
As I already mentioned, the stop()
and interrupt()
methods found in the J2SE Thread
class are not available in either J2ME configuration. The stop()
method has been deprecated because it is inherently unreliable and cannot be implemented on all platforms safely and consistently. The interrupt()
method has been reintroduced in version 1.1 of the CLDC and will probably show up in the next revision of the CDC.
?
Once started, then, a J2ME thread lives until it intentionally or unintentionally (as a result of an uncaught exception) exits the run()
method it invoked on startup. (The system may also terminate running threads when the application stops running, of course, but that is really another case of unintentional exit.) There is no way for one thread to force another thread to stop: You can stop a live thread only by convincing it to terminate itself.
?
It's important to provide a way to stop an application's threads in short order. The simplest way is to have each thread periodically check a boolean variable:
<!-- -->
public class MyThread implements Runnable {
private boolean quit = false;
public void run(){
while( !quit ){
// do something
}
}
public void quit(){
quit = true;
}
}
|
<!-- -->
This works well when only one thread at a time executes a given run()
method, but it fails when multiple threads share the same run()
method (in other words, share the same Runnable
object) and you want only some of the threads to stop. For example, it's common to want all but the most recent thread to stop executing. Instead of using a boolean variable, store a reference to the most recently created thread, and in the run()
method check to see whether it matches the current thread instance:
<!-- -->
public class AnotherThread implements Runnable {
private Thread mostRecent = null;
public Thread restart(){
mostRecent = new Thread( this );
mostRecent.start();
}
public void run(){
Thread thisThread = Thread.currentThread();
while( mostRecent == thisThread ){
// do something
}
}
public void stopAll(){
mostRecent = null;
}
}
|
<!-- -->
It often makes sense to perform the check several times within the loop, before and after lengthy operations, and to break out of the loop immediately:
<!-- -->
...
while( mostRecent == thisThread ){
doLengthyOperation1();
if( mostRecent != thisThread ) break;
doLengthyOperation2();
if( mostRecent != thisThread ) break;
doLengthyOperation3();
}
...
|
<!-- -->
Normally, you want a thread to exit as quickly as possible, delaying only enough to clean up after itself.
?
If a thread is using any kind of system resource - such as a network connection or a record store - that it created or opened itself, remember to use a finally
block to release those resources properly no matter how the thread is terminated. For example:
<!-- -->
public void run(){
RecordStore rs = null;
try {
rs = RecordStore.openRecordStore("myrs", true );
// do something with the record store...
}
finally {
if( rs != null ){
try {
rs.close();
}
catch( RecordStoreException e ){}
}
}
}
|
<!-- -->
Otherwise, the first thread that exits when an exception isn't caught may prevent other threads from accessing the resource. Using a finally
block also avoids memory leaks that can cause the application to fail when run over long periods. Releasing resources and avoiding memory leaks is especially important in J2ME applications. Some J2ME-enabled phones, for example, limit applications to one or two active network connections at any given time.
?
Once one thread has "asked" another thread to stop, the first thread can see whether the second is still alive by calling its isAlive()
method. If the first thread needs to wait for the second thread to terminate before proceeding, it can call the thread's join()
method:
<!-- -->
...
MyThread other = .... // some other active thread
other.quit(); // tell it to quit
other.join(); // wait 'til it does
...
|
<!-- -->
Be careful, though: If you invoke join()
on a thread that isn't stopping, you'll just end up blocking the calling thread indefinitely.
<!-- -->
?
Thread Synchronization
Starting and stopping threads is easy. The hard part is getting threads to interact safely. Because a context switch can occur at any time, the newly running thread can find itself reading data that a previously running thread was not done writing. Interacting threads must use thread synchronization
to control who can read or write shared data at any given time.
?
Thread synchronization depends on a construct called a monitor
. A monitor acts as a gatekeeper, ensuring that only one thread at a time has access to data. Whenever a thread needs to access protected data, it asks the monitor to lock the data for its own use. Other threads interested in the same data are forced to wait until the data is unlocked, at which point the monitor will lock the data for one of the waiting threads and allow it to proceed with its processing.
?
Any Java object can act as a monitor to control access to data using the synchronized
keyword. For example:
<!-- -->
public void appendTime( StringBuffer buf ){
synchronized( buf ){
buf.append( "Current time is: " );
buf.append( System.currentTimeMillis() );
}
}
|
<!-- -->
If two threads call this method simultaneously, one is forced to wait until the other has left the synchronized block.
?
Because any Java object can act as a monitor, the this
reference can be used for synchronization within a non-static method:
<!-- -->
public class Counter {
private int counter;
public int increment(){
synchronized( this ){
return ++counter;
}
}
public int decrement(){
synchronized( this ){
if( --counter < 0 ){
counter = 0;
}
return counter;
}
}
}
|
<!-- -->
Synchronizing with this
is a simple way for a class to control access to its internal state. The Java language even provides a shortcut: Instead of enclosing the contents of a method in a synchronized block, you can add the synchronized
keyword to the method declaration. For example, the Counter
class can be written more simply:
<!-- -->
public class Counter {
private int counter;
public synchronized int increment(){
return ++counter;
}
public synchronized int decrement(){
if( --counter < 0 ){
counter = 0;
}
return counter;
}
}
|
<!-- -->
Static methods can be declared this way as well, even though there is no this
object to use as a monitor:
<!-- -->
public class SomeClass {
public static synchronized void someMethod(){
// do something
}
}
|
<!-- -->
Instead, static methods use the class object of the class in which they are declared. The example above is equivalent to:
<!-- -->
public class SomeClass {
public static void someMethod(){
synchronized( SomeClass.getClass() ){
// do something
}
}
}
|
<!-- -->
Many of the core Java classes use synchronized methods. The java.lang.StringBuffer
class is extensively synchronized, for example. The synchronization carries over to the J2ME versions of those classes. Remember, however, that method synchronization does not guarantee the order in which different threads invoke the object's methods, only that only one thread will be executing any of the synchronized methods at a time. Recall the appendTime()
example:
<!-- -->
public void appendTime( StringBuffer buf ){
synchronized( buf ){
buf.append( "Current time is: " );
buf.append( System.currentTimeMillis() );
}
}
|
<!-- -->
Although StringBuffer.append()
is synchronized, guaranteeing that each value passed to it is appended to the string buffer as a single unit, appendTime()
needs the synchronized block to ensure that the sequence
of calls is also made as a single unit.
?
Thread synchronization works only when threads cooperate. To protect mutable data (data that can be changed) properly, all
threads accessing the data must access it through synchronized code blocks. If the data is encapsulated in a class, this is easily done by making all (or most) of the methods synchronized - but make sure you understand which monitor is being used to do the synchronization. For example, notice that a significant change occurs when you remove the synchronized block from appendTime()
and declare the method itself to be synchronized
:
<!-- -->
public synchronized void appendTime(
StringBuffer buf ){
buf.append( "Current time is " );
buf.append( System.currentTimeMillis() );
}
|
<!-- -->
This version of appendTime()
and the preceding one are not
equivalent, because they are synchronizing using different monitors. The new version is actually equivalent to:
<!-- -->
public void appendTime( StringBuffer buf ){
synchronized( this ){
buf.append( "Current time is " );
buf.append( System.currentTimeMillis() );
}
}
|
<!-- -->
The first version precluded any other changes to the string buffer while a thread was executing appendTime()
, but the second version only ensures that different threads invoking appendTime()
on the same class instance will not interfere with each other. Another thread with access to the same string buffer could still interfere with the thread actually executing the appendTime
method.
?
Of course, thread synchronization comes at a cost. Locking and unlocking data takes time, even on
the fastest of processors. If your classes are immutable
- their external state does not change after initialization - then you can often dispense with synchronization. The java.lang.String
class, for example, does not define any synchronized methods. Data that is not accessed by more than one thread does not need to be synchronized, either.
?
Thread synchronization does entail one danger: Two threads can deadlock
if each attempts to lock data already locked by the other. Suppose thread A locks object X and thread B locks object Y, and then A attempts to lock Y at the same time B tries to lock X. Both threads block, and there's no way to unblock them. Deadlock avoidance is an important consideration when designing your application. A simple avoidance technique is to lock objects in the same order every time - always lock X before Y, for example. For more information about deadlock and deadlock avoidance, see the Resources section.
<!-- -->
?
Waiting and Notifications
Synchronization controls access to shared data, but often you want a thread to wait for an event to occur before accessing the data. A common solution is to have a thread read and process commands from a queue, commands placed there by other threads. If you use a java.util.Vector
as the queue, a simple approach is to use an endless loop:
<!-- -->
import java.util.Vector;
public class Worker implements Runnable {
private boolean quit = false;
private Vector queue = new Vector();
private Thread thread;
public Worker(){
new Thread( this ).start();
}
public void run(){
Object o;
while( !quit ){ // busy wait!
o = null;
synchronized( queue ){
if( queue.size() > 0 ){
o = queue.elementAt( 0 );
queue.removeElementAt( 0 );
}
}
if( o != null ){
// do something
}
}
}
public boolean addToQueue( Object o ){
if( !quit ){
queue.addElement( o );
return true;
}
return false;
}
public void quit(){
quit = true;
}
}
|
<!-- -->
This kind of loop is known as a busy wait
, because the thread is always busy executing code. You want to avoid busy waits because they waste valuable processor cycles that other threads could be putting to good use. What you want is a suspended wait
, where the thread is suspended until the desired event occurs. Suspended threads do not affect the scheduling of other threads.
?
As it happens, the monitors used for thread synchronization can also be used for thread suspension. If you think about it, thread synchronization is really just a special case of thread suspension, because each thread entering a synchronized block waits its turn for access to the data. The monitor maintains a queue of waiting threads, allowing only one thread at a time to enter the block.
?
Because every Java object can act as a monitor, the java.lang.Object
class defines three methods that expose this basic functionality: wait()
, notify()
, and notifyAll()
. Any thread can suspend itself by calling an object's wait()
method:
<!-- -->
...
Object obj = .... // some object to use as a lock
synchronized( obj ){
// here is where you'd check obj's state
try {
obj.wait();
}
catch( InterruptedException e ){
}
}
...
|
<!-- -->
The thread must lock the object before invoking its wait()
method. It must also catch the java.lang.InterruptedException
and deal appropriately with thread interruptions. The thread implicitly releases its lock on the object after it suspends itself.
?
The wait()
method is overloaded: The thread can specify an optional timeout in milliseconds if it doesn't want to wait indefinitely.
?
Once a thread suspends itself, another thread releases it by invoking the same object's notify()
or notifyAll()
method:
<!-- -->
...
Object obj = .... // same object used as lock!
synchronized( obj ){
obj.notify(); // or notifyAll
}
...
|
<!-- -->
Again, the second thread must lock the object before calling notify()
or notifyAll()
. These two methods behave the same except that one wakes one waiting thread while the other wakes all waiting threads. The order in which threads are woken is not specified. Each newly woken thread must re-obtain its lock on the object before it can actually proceed, because its lock on the object was implicitly released when it suspended itself.
?
Armed with this knowledge, you can rework the Worker
class to avoid busy waiting:
<!-- -->
import java.util.Vector;
public class Worker implements Runnable {
private boolean quit = false;
private Vector queue = new Vector();
public Worker(){
new Thread( this ).start();
}
public void run(){
Object o;
while( !quit ){
o = null;
synchronized( queue ){
if( queue.size() > 0 ){
o = queue.elementAt( 0 );
queue.removeElementAt( 0 );
} else {
try {
queue.wait();
}
catch( InterruptedException e ){
}
}
}
if( o != null ){
// do something
}
}
}
public boolean addToQueue( Object o ){
synchronized( queue ){
if( !quit ){
queue.addElement( o );
queue.notify();
return true;
}
return false;
}
}
public void quit(){
synchronized( queue ){
quit = true;
queue.notify();
}
}
}
|
<!-- -->
This is a much better behaved version, because the worker thread executes only when there are items in the queue.
?
As a rule, you should avoid suspending threads that your application did not create. System-defined threads - including those that deliver user-interface events and other notifications - are often shared among multiple applications. Suspending a system thread can affect the user interface, making the application appear to lock up. It may also prevent critical notifications from being delivered to other applications. Read the article Networking, User Experience, and Threads
for more details.
<!-- -->
?
A Real-Life Example
I'll conclude this look at threads with a real-world example involving the Wireless Messaging API (WMA), a J2ME optional package that enables applications to send and receive Short Message Service (SMS) messages.
?
An application using the WMA can register to be notified whenever a message arrives for it. The WMA defines a MessageListener
interface:
<!-- -->
package javax.wireless.messaging;
public interface MessageListener {
void notifyIncomingMessage(
MessageConnection conn );
}
|
<!-- -->
The application creates an object that implements this interface and registers it with the WMA subsystem. When a message arrives for the application, the WMA subsystem invokes the notifyIncomingMessage()
method on a thread of its own choosing. The WMA specification does not allow the application to receive or process the message on its thread - the system is merely notifying
the application that a message has arrived. The application must use a separate thread to handle the message. Here's one way to do this processing:
<!-- -->
import java.io.IOException;
import java.util.Vector;
import javax.wireless.messaging.*;
// A message receiver waits for messages to arrive
// on a message connection and then uses a separate
// thread to receive the actual message.
public class MessageQueuer
implements Runnable, MessageListener {
// A data structure representing either a
// connection-message pair (if a message was read)
// or a connection-exception pair (if an error
// occurred during the reading).
public static class Entry {
MessageConnection connection;
Message message;
IOException exception;
}
private Vector incoming = new Vector();
private Vector outgoing;
private boolean stop;
// Pass the outgoing vector into the constructor.
public MessageQueuer( Vector outgoing ){
this.outgoing = outgoing;
new Thread( this ).start();
}
// Called whenever a message arrives for the
// given connection. Queues the connection object
// and wakes up the thread to actually receive
// the message.
public void notifyIncomingMessage(
MessageConnection conn ){
if( !stop ){
synchronized( incoming ){
incoming.addElement( conn );
incoming.notify();
}
}
}
// Waits for incoming connection objects to be
// queued. When one arrives, pulls it off the queue
// and receives the messags. Queues the resulting
// connection-message pair on the outgoing
// queue and wakes anyone listening to the queue.
public void run(){
while( !stop || !incoming.isEmpty() ){
MessageConnection conn = null;
synchronized( incoming ){
while( !stop && incoming.isEmpty() ){
try {
incoming.wait();
}
catch( InterruptedException e ){
}
}
if( !incoming.isEmpty() ){
conn = (MessageConnection)
incoming.elementAt( 0 );
incoming.removeElementAt( 0 );
}
}
if( conn == null ) continue;
Entry entry = new Entry();
entry.connection = conn;
try {
entry.message = conn.receive();
}
catch( IOException e ){
entry.exception = e;
}
synchronized( outgoing ){
outgoing.addElement( entry );
outgoing.notify();
}
}
}
// Stops the processing of messages.
public void stop(){
synchronized( incoming ){
stop = true;
incoming.notify();
}
}
}
|
<!-- -->
The MessageQueuer
class looks complicated, but it's not - it follows the same basic outline as the Worker
class we just created. The MessageQueuer
's constructor takes a Vector
to serve as the outgoing queue, where received messages are placed. Typical setup code:
<!-- -->
...
Vector receiver = new Vector();
MessageQueuer queuer = new MessageQueuer( receivef );
MessageConnection conn1 = ... // a server connection
MessageConnection conn2 = ... // another connection
conn1.setMessageListener( queuer );
conn2.setMessageListener( queuer );
...
|
<!-- -->
Notice how the MessageQueuer
can listen to more than one connection. When a message arrives at a connection, the queuer receives it and places an instance of MessageQueuer.Entry
on the outgoing queue. Another thread can wait on the outgoing queue:
<!-- -->
...
Vector receiver = .... // the receiving queue
boolean quit = false;
while( !quit ){
MessageQueuer.Entry entry;
synchronized( receiver ){
while( receiver.isEmpty() ){
try {
receiver.wait();
}
catch( InterruptedException e ){
}
}
entry = (MessageQueuer.Entry)
receiver.elementAt( 0 );
receiver.removeElementAt( 0 );
}
if( entry.message != null ){
// handle entry.message here
} else {
// handle entry.exception here
}
entry = null;
}
...
|
<!-- -->
Although this code fragment doesn't show it, the real advantage to the MessageQueuer
class is that it lets an application scan through the list of received messages before deciding which one to process. The application could give priority to messages arriving on a certain connection, for example.
?
An alternative implementation is to write a message receiver that creates a new thread to read and process each message as it arrives. Note that threads may be slow to create and individual devices may limit the number of threads that are active at any given time, so this technique may not be practical if many messages are to be received in a short timespan.
<!-- -->
?
Resources
The Threads and Multithreading
section of Sun's Java Developer web site is also a good resource for more information, as are the following books:
-
Concurrent Programming in Java
, by Doug Lea
-
Java Thread Programming
, by Paul Hyde
-
Taming Java Threads
, by Allen Holub
<!-- --><!-- -->
<!-- --><!-- -->About the Author:
Eric Giguere
is a software developer for iAnywhere Solutions, a subsidiary of Sybase, where he works on Java technologies for handheld and wireless computing. He holds BMath and MMath degrees in Computer Science from the University of Waterloo and has written extensively on computing topics.
?
原文:http://developers.sun.com/mobility/midp/articles/threading2/
|