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

 
Contents:
 Why wait in line?
 Implementing threads
 Advanced multi-threading
 Deprecated methods
 Debugging threads
 Debugging several threads
 Limits to thread priority
 Conclusion
 Resources
 About the author

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
[email protected].




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.


Send your comments to: [email protected]
Bill Bilow's Java Table © 1996-1997: William A. Bilow, Jr.

This site is brought to you courtesy of GeoCities. Get your own Free Home Page.


Home

Last modified 06/11/2000

Hosted by www.Geocities.ws

/HTML> 1