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.
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) {
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:
one thread calling a method on the other
shared objects -- one thread calls a method on the object
to mutate it in some way, and another object later calls a method
on the object and observes the change
events -- one thread (the source) causes an event and a method
on the other thread (the listener) is called to notify it of the event
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.