Overview of Java exceptions and threads

Exceptions and threads are not particularly related to each other. Joining them in the same lecture is purely coincidental.

Java exceptions

A Java exception is a run-time error. Java exception handling system allows you to handle exceptions in a structured and controlled manner. It is done by defining a block of code called exception handler which is executed automatically when an error occurs. Java conveniently defines exceptions for many common program errors, s.a. division by zero or file-not-found.

Java exceptions are derived from a class called Throwable. It has two subclasses: Error, which deals with errors in the Java virtual machine (those errors are not related to the program code and are beyond a programmer's control) and Exception. The class Exception includes standard kinds of exceptions, s.a. ArithmeticException and ArrayIndexOutOfBounds. A programmer may define his/her own exceptions by extending the class Exception.

A block of code which is monitored for exceptions is included between the keywords try and catch. There may be several catch statements for each try, as long as they catch different kinds of exceptions:


try {
	for (int i = 0; i < 10; ++i) {
		System.out.println(numer[i] + " / " + denom[i] + " = " + numer[i]/denom[i]);
	}
} 
catch (ArithmeticException e) {
	System.out.println("Division by zero!");
}
catch (ArrayIndexOutOfBounds e) {
	System.out.println("Can't go out of the array boundaries!");
}
Here for each element of the array denom which is 0, the program will print the message "Division by zero!", and if at least one of the arrays is shorter than 10 elements, the message "Can't go out of the array boundaries!" will be printed.

Instead of using your own message, as in the example above, you can also just pass the exception as a parameter to println, and a standard message for the exception will be printed. You can also invoke a method printStackTrace() on an exception to print out the sequence of method invocations that the exception occured within. To catch any possible exception, use catch (Throwable e).

You can declare your own exceptions by extending the class Exception. In order to display your own exception message, you need to override the default toString() method of an exception. You may use an exception to check for certain conditions in your program. For instance, you if a method requires that a parameter is a positive integer, you can throw an exception if it is not. Exceptions are objects, so in order to throw an exception, you need to create a new exception object first.
Click here for an example.

If a method throws an exception which it does not catch, and the exception is not of the class Error or RuntimeException, then the method declaration must specify the exception that it throws, for instance:


public void mymethod(...) throws Exception1, Exception2 {
	//body
}

What are Java threads

In a single-threaded program the flow of control moves from one method to another, but only one method is running at any given time. In a multithreaded program several methods may be in execution simultaneously, i.e. a program might be performing a computation and drawing a picture on the screen at the same time. Several threads of execution may be completely independent from one another, or may pass information to one another (for instance, the display thread periodically updates the picture based on the intermediate results of the computation thread).

An important advantage of using threads is that they allow the program to utilize time which would otherwise be spent waiting for an I/O device or some other slower process.

Threads are also called light-weight processes (LWP). They provide convenience of multi-processing (i.e. creating multiple processes run separately by the operating system) without the overhead of setting up a new process. Communication between threads and sharing of resources is also easier than that between separate processes.

Java provides classes and methods that allow creating and manipulating threads. While these classes and methods are part of the Java standard, one has to keep in mind that the actual implementation of threads depends on the operating system, and therefore thread behaviour differs from one machine to another for the same version of Java.

Thread methods and life cycle

A thread can be in one of the following states:
  1. newly created
  2. running on the CPU.
  3. ready to run, waiting for CPU time.
  4. suspended, temporarily stopped execution, possible to let a thread with a higher priority run. A suspended thread can later be resumed.
  5. blocked, waiting for a resource or an event.
  6. terminated, the execution has ended and cannot be resumed.
A thread's states and behaviour are controled by the following methods:
  1. thread constructor. Some versions of the constructor are: Every thread has a name. If it's not passed to the constructor, then a default name is given. You can change it by using setName(String name) method. We discuss Runnable below.
  2. void run().
    The method where the behaviour of the thread is defined. Is invoked when the thread starts running.run() can pass control to other methods, in which case if the thread is suspended, and then resume, it will continue from the point at which it has been suspended.
  3. void start().
    Starts a newly created thread by invoking its run() method. If the thread can get CPU time, it becomes running, otherwise it becomes ready to run.
  4. static void sleep(long milliseconds) throws InterruptedException
    Suspends the thread for the specified time. The thread goes from running to suspended. The method is static, i.e. it should be invoked on the class, as follows: Thread.sleep(100). It suspends the thread in which it is invoked. Because it throws an exception which is not in RuntimeException, it has to be inside a try/catch block.
  5. final void join() throws InterruptedException
    The method waits until the thread on which it's called terminates. The waiting thread becomes blocked. For instance, by putting thr.join() in the end of main we guarantee that main is not going to terminate until thr does.
  6. static void yield().
    Causes the currently running thread to pause temporarily to let the other threads run. The thread becomes runnable, but not running.

Two ways of implementing threads in Java

Every Java application has at least one thread, which is the main thread. It starts when the main method starts executing, and stops when it terminates. If a programmer needs additional threads, there are two ways of creating them:

Access control using the synchronized keyword

If several threads execute methods of the same object simultaneously in an uncontrolled way, it is possible that some updates to the object will be lost. Consider the following example:

     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) {
                 if (amt < amount) 
                      amount -= amt;
             }
             public float checkBalance() {
                     return amount;
             }
     }
Here we can imagine two threads accessing an instance of the class Account via the withdraw method, both trying to withdraw exactly the amount currently on the account. First both threads check if there is sufficient amount for the withdrawal, and both get a positive answer. Then one withdraws money, and then the other. The total on the account is negative! There are other possible bad scenarios involving simultaneous deposits and withdrawals.

We can prevent this scenario by "locking" the account so that only one thread at a time can perform deposits or withdrawals. Each object in Java has a monitor associated with it, and this monitor can be locked by invoking a method declared as "syncronized". If the object is locked, then no syncronized methods can access this object until the method that has locked the object terminates, thus releasing the lock. In the following code if one thread performs a deposit or a withdrawal from the account, no other thread can perform deposits or withdrawals. However, checkBalance() can be performed at any time, since it does not need lock for an the account.


     public class Account {
             String holderName;
             float amount;
             public Account(String name, float amt) {
                     holderName = name;
                     amount = amt;
             }
             public syncronized void deposit(float amt) {
                     amount += amt;
             }
             public syncronized void withdraw(float amt) {
                      if (amt < amount) 
                             amount -= amt;
             }
             public float checkBalance() {
                     return amount;
             }
     }

Now threads that want to access the account are syncronized on this account, i.e. the account's monitor locks the account when a thread invokes its withdraw or deposit method.

It is also possible to prevent simultaneous access to a block of code as follows:


        syncronized(object) {
              // statements to be syncronized
				}
Note that we need to specify the object on which to syncronize.

When a thread is waiting to execute a syncronized method on an object that's currently locked, the thread is blocked

Threads communication using wait() and notify()

Often locking a shared resource is not sufficient to guarantee a desired behaviour of threads. Consider a following well-known example of a producer and a consumer: a producer thread produces some data and puts it into a buffer. The buffer can hold one piece of data at a time. A consumer thread takes the data out of the buffer. Then the producer can put another piece of data, and so on. Here is a "solution" which is based on just locking the buffer object via syncronized put and get method. This solution does not provide the right thread behaviour, because all it guarantees is that the consumer and the producer thread don't access the buffer simultaneously, but it does not specify the order in which they access it, so the producer might put two pieces of data in the buffer before the consumer takes a data out, or a consumer might attempt to get data out of an empty buffer.

The following three methods of class Object allow threads to communicate so that they can coordinate their access to the buffer:

These methods are invoked on the object for which the access has to be syncronized. In this case the object is the buffer. These methods can be invoked only within a synchronized method.

Here is a solution of the consumer/producer problem which uses wait() and notify().

Thread priority

Every set has a priority: an integer number from 1 to 10 associated with it. The default priority is 5. If two threads are ready to tun, and CPU becomes available, then the higher priority thread gets to run. Priorities are manipulated by setPriority and getPriority methods of threads. A priority of a thread can be reset at any point in a program.

Deadlocks and infinite postponement

An example of a deadlock situation is when a thread A is executing a synchronized method a of an object O1 and needs to invoke a synchronized method b of an object O2, whereas a thread B is executing method b of O2 and needs to invoke the method a of O1. This situation cannot be resolved, because none of the threads can proceded before the other releases the lock. Deadlocks can be a problem in multithreaded programming, and one has to be very careful to make sure that a deadlock cannot occur.

Another problem that one might encounter in multithreaded programming is infinite postponement: a thread never gets to run because other threads with higher priority always get to run first. Again, care must be taken to avoid this problem.


Some material on this page has been adopted from a subset of online sources listed here
This page has been created and is maintained by Elena Machkasova
Comments and suggestions are welcome at emachkas@wellesley.edu

Spring Semester 2002