CS102: Concurrent Systems: Multithreaded Applications

Copyright © 1999, Kenneth J. Goldman

Threads

A concurrent system is one in which there are multiple computations proceeding in parallel.


When these computations coexist within the same application (in one address space), we call them threads. "Thread" is short for "thread of control" -- the image in your mind should be an actual strand of thread that winds through the statements of your program. Different threads may start in different places, but may visit a lot of code in common. The steps of each thread are interleaved with other threads.






Thread synchronization is about controlling how the threads interleave their executions.



Creating Threads

In JAVA, all threads are created using the class Thread. To create a thread, you can extend the Thread class, overriding the run() method with the the computation you want the thread to perform. For example, the following thread moves a Component to a given x,y location gradually.


public class ComponentMover extends Thread {

static final int FRAMES_PER_SECOND = 20;
private int x,y;
private Component c;
private int pixelsPerSecond;

public ComponentMover(Component c, int x, int y, int pixelsPerSecond) {
this.c = c;
this.x = x;
this.y = y;
this.pixelsPerSecond = pixelsPerSecond;
}

public void run() {
double curX = c.getBounds().x;
double curY = c.getBounds().y;
double distance = Math.sqrt(Math.pow(x-curX,2)+Math.pow(y-curY,2));
double nSteps = distance / pixelsPerSecond * FRAMES_PER_SECOND;
double deltaX = (x-curX) / nSteps;
double deltaY = (y-curY) / nSteps;
while ((Math.abs(x-curX) > Math.abs(deltaX)) ||
(Math.abs(y-curY) > Math.abs(deltaY)) {
curX += deltaX;
curY += deltaY;
c.setLocation((int)curX, (int)curY);
try {
sleep(1000/FRAMES_PER_SECOND);
}
catch (InterruptedException ie) {
...
}
}
//end while
c.setLocation(x,y);
}

}

To make the thread run, you'd write


Thread t = new ComponentMover(myComponent, 520, 350, 60);
t.start();

The thread would continue running until the run method returns, or until one of the following happens:


t.stop() -- kills the thread
t.suspend()
-- makes the thread pause until someone calls t.resume() --awakens a suspended thread

Note: Sometimes you want to have a run() method in a class that extends some class other than Thread. You can do this by implements the Runnable interface and passing the Runnable object to the Thread constructor. For example,


public class ComponentMover extends Foo implements Runnable {

...

}

Thread t = new Thread(new ComponentMover(myComponent, 520, 250, 60));
t.start();

Sometimes, multiple threads that coexist in an application work completely independently, but often we want to achieve communication among threads so the communication among threads so the computation in each thread may be affected by computation (or events) that occur in other threads.


This communication is often achieved through:



Thread Synchronization

Remember that the instructions of different threads are interleaved, so it's important to make sure that one thread doesn't run while another thread is in the middle of an atomic update, something that should appear to happen in one step. Otherwise an inconsistency might be observed, with unpredictable results.


For example, suppose we have a queue ADT, and one thread is putting data into the queue, while another is taking data from the queue and processing it. This is known as a producer / consumer computation.


q is a shared queue
Thread A:

public void run() {
while (true) {
x = nextDataItem();
q.enqueue(x);
}
}
Thread B:

public void run() {
while (true) {
x = q.dequeue();
processDataItem(x);
}
}

Now suppose the enqueue and dequeue methods look like this:


public class Queue {
...
...

public void enqueue (Object x) {
numObjects++; // rep. invariant -- equals # of items in queue
put x at the rear of the list;
}

public Object dequeue () {
Object result = null;
if (numObjects > 0) {
numObjects--;
result = object off front of list;
}
return result;
}
}

On the surface, this looks fine -- the producer puts things into the queue and the consumer read from the queue.


But a closer look reveals a potentially dangerous situation. Suppose that the queue is empty. Now suppose that the producer thread (Thread A) begins to run, ...


Thread A numObjects Thread B
0
while (true) {
x = nextDataItem();
q.enqueue(x); ...
numObjects++;
1
0 while (true) {
x = q.dequeue();
if (numObjects > 0) {
numObjects--;
...

B gets a NullPointerException in dequeue because there is no item (yet) on the front of the list. Furthermore, once A adds the item, numObjects is 0, so B won't notice that object (unless another one is added) since the count is off by one.


In other words, thread B observed the state of the queue in the middle of a method, while the rep. invariant was false!


We can make methods appear to execute atomically by putting the modifier synchronized in front of the method declaration. For example,


public synchronized void enqueue (Object x) {...}
public synchronized Object dequeue () {...}

When a method is declared to be synchronized, JAVA will make other threads wait while a thread is executing inside the method. In fact, it won't let another thread call any synchronized method on that object during execution of a synchronized method.


However, we can't just blindly put synchronized modifiers on every method, or threads could end up waiting for each other indefinitely, a situation known as deadlock, which we consider next.