Original URL : http://www-4.ibm.com/software/developer/library/multithreading.html
Multi-threading in Java programs
See how easy it is to develop and use threads
Neel V. Kumar
Software engineer, Terway.com
March 2000
Using multiple threads in Java programs is far easier than in C or C++ because of the language-level support offered by the Java programming language. This article uses simple programming examples to demonstrate how intuitive threading in Java programs is. Users should be able to write simple, multi-threaded programs after studying this article.
Why wait in line?
The simple Java program below does four unrelated tasks. Such a program has a
single thread of control and the control moves linearly over the four tasks.
Moreover, each of the tasks involves significant waiting time because the
resources needed -- printer, disk, database, and screen -- have built-in
latencies due to hardware and software limitations. So, the program needs to
wait for the printer to finish printing the file before the database can be
accessed, and so on. This is a poor use of computing resources and also of your
time if you are waiting for the program to finish. One way to improve this
program is to make it multi-threaded.
Four unrelated tasks
class myclass {
static public void main(String args[]) {
print_a_file();
manipulate_another_file();
access_database();
draw_picture_on_screen();
}
}
|
In this example, each task must wait for the previous one to finish before starting, even when the tasks involved are unrelated. However, in real life, we are constantly using a multi-threaded model. We send our children, spouses, and parents on errands while working on some other task. For example, I might send my son to the post office to purchase stamps while I finish writing the letter to be mailed. In software terms, this is known as multiple threads of control (or execution).
There are two different ways of achieving multiple threads of control:
Implementing threads using the Java programming
language
The Java programming language makes multi-threading so simple and effective that
some programmers say that it is effectively free. While threads are far easier
to use than in other languages, there are still some constructs that need to be
handled. The important thing to remember is that the main() function is
also a thread and can be employed to do useful work. The programmer needs to
create new threads only if more than one thread is needed.
Thread class
The Thread class is a concrete class, that is, not an abstract class, which
encapsulates the behavior of a thread. To create a thread, the programmer must
create a new class derived from Thread class. The programmer must override the run()
function of Thread to do useful work. This function is not called directly by
the user; rather, the user must call the start() function of thread,
which in turn calls run(). The following code illustrates the use:
Creating two new threads
import java.util.*;
class TimePrinter extends Thread {
int pauseTime;
String name;
public TimePrinter(int x, String n) {
pauseTime = x;
name = n;
}
public void run() {
while(true) {
try {
System.out.println(name + ":" + new
Date(System.currentTimeMillis()));
Thread.sleep(pauseTime);
} catch(Exception e) {
System.out.println(e);
}
}
}
static public void main(String args[]) {
TimePrinter tp1 = new TimePrinter(1000, "Fast Guy");
tp1.start();
TimePrinter tp2 = new TimePrinter(3000, "Slow Guy");
tp2.start();
}
}
|
In this example, we can see a simple program that prints the current time on the screen at two different time intervals (1 and 3 seconds). This is achieved by creating two new threads, for a total of three including main(). However, this mechanism for creating threads is sometimes not possible because the class you want to run as a thread may already be part of a class hierarchy. The Java programming language allows a class to have only one parent class, although any number of interfaces can be implemented in the same class. Also, some programmers eschew derivation from Thread class because it intrudes upon the class hierarchy. For such a situation, the runnable interface is used.
Runnable interface
This interface has a single function, run(), which must be implemented
by the class that implements the interface. However, when it comes to running
the class, the semantics are slightly different from the previous example. We
can redo the previous example using the runnable interface. (Differences are in
bold.)
Creating two new threads without intruding on class hierarchy
import java.util.*;
class TimePrinter implements Runnable {
int pauseTime;
String name;
public TimePrinter(int x, String n) {
pauseTime = x;
name = n;
}
public void run() {
while(true) {
try {
System.out.println(name + ":" + new
Date(System.currentTimeMillis()));
Thread.sleep(pauseTime);
} catch(Exception e) {
System.out.println(e);
}
}
}
static public void main(String args[]) {
Thread t1 = new Thread(new TimePrinter(1000, "Fast Guy"));
t1.start();
Thread t2 = new Thread(new TimePrinter(3000, "Slow Guy"));
t2.start();
}
}
|
Note that when using the runnable interface, you cannot directly create an object of the desired class and run it; rather, it has to be run from within an instance of Thread class. Many programmers prefer runnable interface because inheritance from Thread class intrudes upon the class hierarchy.
The synchronized keyword
The examples we have seen so far make use of threads in a simplistic manner.
There is minimal data flow and no chance of two threads accessing the same
object. However, in most useful programs, there is usually a flow of information
between threads. Consider a banking application, which has an Account object as
shown in the next example:
Multiple activities in a bank
public class Account {
String holderName;
float amount;
public Account(String name, float amt) {
holderName = name;
amount = amt;
}
public void deposit(float amt) {
amount += amt;
}
public void withdraw(float amt) {
amount -= amt;
}
public float checkBalance() {
return amount;
}
}
|
There is a bug lurking in that sample code. If this class is used in a single-threaded application, there are no problems. However, in the case of a multi-threaded application, it is possible for the same Account object to be accessed by different threads at the same time, say due to simultaneous access by owners of a joint account at different ATMs. In that situation, deposits and withdrawals could take place in such a manner that a transaction is overwritten by another transaction. Such a situation would be a disaster. However, the Java programming language offers a simple mechanism to prevent such overwriting. Each object, at run time, has an associated lock. This lock can be obtained by adding the keyword synchronized to a method. So, the revised Account object, as shown below, will not suffer from the data corruption bug:
Synchronizing multiple activities in a bank
public class Account {
String holderName;
float amount;
public Account(String name, float amt) {
holderName = name;
amount = amt;
}
public synchronized void deposit(float amt) {
amount += amt;
}
public synchronized void withdraw(float amt) {
amount -= amt;
}
public float checkBalance() {
return amount;
}
}
|
Both deposit() and withdraw() functions need the lock to operate so that one is blocked when the other function is running. Note that checkBalance() has not been changed because it is strictly an accessor function. Because checkBalance() is not synchronized, it is neither blocked by any method nor does it block any other method, whether synchronized or not.
Advanced multi-threading support in the Java programming language
Threadgroups
Threads are created individually but can be grouped into threadgroups for
ease of debugging and monitoring. A thread can be associated with a threadgroup
only when the thread is created. In programs that use a large number of threads,
organization using threadgroups can be helpful. Think of them as directory and
file structure on a computer.
Inter-thread signaling
When a thread needs to wait for a condition before proceeding, the synchronized
keyword is not enough. While the synchronized keyword prevents
concurrent updates to an object, it does not implement inter-thread signaling.
For such use there are three functions provided by the Object class: wait(),
notify(), and notifyAll(). Take the example of a global
climate forecasting program. These programs usually divide the globe into many
cells and in each iteration, the computation of each cell is done in isolation
until the values stabilize and then some data exchange takes place between
adjacent cells. So, in essence, in each iteration, the threads have to wait for
all the threads to finish their assigned task before continuing on to the next
iteration. This model is known as barrier synchronization, which is
illustrated in the following example:
Barrier synchronization
public class BSync {
int totalThreads;
int currentThreads;
public BSync(int x) {
totalThreads = x;
currentThreads = 0;
}
public synchronized void waitForAll() {
currentThreads++;
if(currentThreads < totalThreads) {
try {
wait();
} catch (Exception e) {}
}
else {
currentThreads = 0;
notifyAll();
}
}
}
|
When wait() is called from a thread, that thread is effectively blocked till some other thread calls notify() or notifyAll() on the same object. So, in the previous example, different threads would call the waitForAll() function as their work gets done and the last thread would trigger the notifyAll() function which releases all the threads. The third function, notify(), notifies only a single waiting thread, which is useful when restricting access to a resource that only one thread can use at one time. However, it is not possible to predict which thread gets the notification because that depends on the scheduling algorithm of the Java Virtual Machine (JVM).
Yielding the CPU to another thread
When a thread relinquishes a scarce resource, such as a database connection or
network port, it can temporarily lower its priority, using the yield()
function, so that some other thread can be run.
Daemon threads
There are two classes of threads: user and daemon. User threads are those
that do useful work. Daemon threads are those threads that serve only in
an auxiliary capacity. To mark a thread as a daemon thread, use the setDaemon()
function provided by the Thread class. A Java program runs till all user threads
have finished and then it destroys all the daemon threads. In a Java Virtual
Machine (JVM), it is possible for a program to continue running even after main
has ended if another user thread is still running. Note that the garbage
collection thread in JVM is a daemon thread, so it is destroyed when all the
user threads have ended.
Avoiding deprecated methods
Deprecated methods are those that are supported for backwards compatibility but
may or may not appear in future revisions. Java multi-threading support has been
significantly revised in versions 1.1 and 1.2, and the stop(), suspend(),
and resume() functions have been deprecated. These functions can
introduce subtle bugs in the JVM. Even though the function names may sound
tempting, resist the temptation to use them.
Debugging threaded programs
Some common, unwanted conditions that occur in threaded programs are deadlocks,
livelocks, memory corruption, and resource exhaustion.
Deadlocks
Deadlocks are perhaps the most common problems with multi-threaded programs.
Deadlocks occur when a thread needs a resource for which another thread has the
lock. This situation is usually very difficult to detect. However, the solution
is quite elegant: acquire all resource locks in the same order in all threads.
For example, if there are four resources -- A, B, C and D -- and a thread may
acquire locks for any of the four resources, ensure that the lock to A is
acquired before the lock to B, and so on. This technique may lead to blocking if
"thread 1" wants locks to B and C while "thread 2" requires
A, C and D, but it would never create deadlocks on these four locks.
Livelocks
A livelock occurs when a thread is so busy accepting new work that it never has
a chance to finish any tasks. The thread ultimately overruns the buffers and
causes the program to crash. Imagine a secretary who needs to type a letter but
is so busy answering the phone that the letter never gets typed.
Memory corruption
The exasperating problem of memory corruption can be avoided completely with
judicious use of the synchronized keyword.
Resource exhaustion
Certain system resources are in limited supply, such as file descriptors. A
multi-threaded program can exhaust the resources because each thread may want to
have one. If the number of threads is rather large or if the candidate threads
for a resource far outnumber the amount of resource available, it is best to use
a resource pool. One of the best examples is a pool of database
connections. Whenever a thread needs to use one, it takes a connection from the
pool, uses it, and then returns it. The resource pool can also be called the resource
library.
Debugging with a large number of threads
Sometimes a program is extremely difficult to debug because of the large number
of threads running. In such situations, the following class may come in handy:
public class Probe extends Thread {
public Probe() {}
public void run() {
while(true) {
Thread[] x = new Thread[100];
Thread.enumerate(x);
for(int i=0; i<100; i++) {
Thread t = x[i];
if(t == null)
break;
else
System.out.println(t.getName() + "\t" + t.getPriority()
+ "\t" + t.isAlive() + "\t" + t.isDaemon());
}
}
}
}
|
Limits to thread priority and scheduling
The Java thread model involves thread priorities that can be dynamically
changed. Basically, the priority of a thread is a number from 1 to 10, with the
higher number indicating a more urgent task. The JVM standard calls for higher
priority threads to be executed before lower priority ones. However, the
standard is silent on the treatment of threads with the same priority. How these
threads are handled depends on the underlying operating system strategy. In some
cases, time-slicing occurs between threads of equal urgency; in other cases, the
threads run to completion. Keep in mind that, while Java supports ten separate
priorities, the underlying operating system may support far fewer, leading to
some confusion. Therefore, use thread priority only as a very coarse tool. Finer
control can be achieved by judicious use of the yield() function. In
general, do not rely on thread priority to control thread states.
Conclusion
This article has shown how to use threads in Java programs. The more important
question of whether threads should be used depends largely on the
application at hand. One approach to deciding whether to use multi-threading in
an application is to estimate the amount of code that can be run in parallel.
Also keep in mind:
It is essential for Internet-based software to be multi-threaded; otherwise, the user feels that the application is sluggish. For example, threading can make programming easier when developing a server that will support a large number of clients. In this case, each thread can service a different client or group of clients, providing a shorter response time.
Some programmers who have used threads in C and other languages, where there is no language support for threads, have been soured toward threads in general. It's different with the Java programming language -- threads in Java programs are much more intuitive and usable.
Resources
About the author
Neel V. Kumar is a software engineer with eight years of experience in
object-oriented programming, using C++ and the Java programming language. A
native of Iowa, he is currently living in Menlo Park, California and working for
a startup in the Telecom field. He has consulted on many projects in a previous
life and likes to share his learning with others. He can be reached at
Next Topic: Accessing Metadata
| Enable Frames | Disable Frames |
| Home Page | JDBC | Java CGI | Java RMI | LiveConnect |
| Web Design | Personal Information | Favorite Links | What's New | Feedback |
visitors since January 1, 1997.
This site is brought to you courtesy of GeoCities. Get your own Free Home Page.
Last modified 06/11/2000