THREADING
Java lets we do several things at once by using threads. If our computer has more than one CPU, it may actually run two or more threads simultaneously. Otherwise, it will switch back and forth among the threads at times that are unpredictable unless we take special precautions to control it.
There are two different ways to create threads. I will only describe one of them here.
Thread t = new Thread(command); //
t.start(); // t start running command, but we don't wait for it to finish
// ... do something else (perhaps start other threads?)
// ... later:
t.join(); // wait for t to finish running commandThe constructor for the built-in class Thread takes one argument, which is any object that has a method called run. This requirement is specified by requiring that command implement the Runnable interface described earlier. (More precisely, command must be an instance of a class that implements Runnable). The way a thread "runs" a command is simply by calling its run() method. It's as simple as that!
In project 1, we are supposed to run each command in a separate thread. Thus we might declare
something like this:
class Command implements Runnable {
String commandLine;
Command(String commandLine) {
this.commandLine = commandLine;
}
public void run() {
// Do what commandLine says to do
}
}We can parse the command string either in the constructor or at the start of the run() method.
The main program loop reads a command line, breaks it up into commands, runs all of the commands concurrently (each in a separate thread), and waits for them to all finish before issuing the next prompt. In outline, it may look like this.
for (;;) {
out.print("% "); out.flush();
String line = inputStream.readLine();
int numberOfCommands = // count how many commands there are on the
line
Thread t[] = new Thread[numberOfCommands];
for (int i=0; i<numberOfCommands; i++) {
String c = // next command on the line
t[i] = new Thread(new Command(c));
t[i].start();
}
for (int i=0; i<numberOfCommands; i++) {
t[i].join();
}
}
There are two different ways to create threads. I will only describe one of them here.
Thread t = new Thread(command); //
t.start(); // t start running command, but we don't wait for it to finish
// ... do something else (perhaps start other threads?)
// ... later:
t.join(); // wait for t to finish running commandThe constructor for the built-in class Thread takes one argument, which is any object that has a method called run. This requirement is specified by requiring that command implement the Runnable interface described earlier. (More precisely, command must be an instance of a class that implements Runnable). The way a thread "runs" a command is simply by calling its run() method. It's as simple as that!
In project 1, we are supposed to run each command in a separate thread. Thus we might declare
something like this:
class Command implements Runnable {
String commandLine;
Command(String commandLine) {
this.commandLine = commandLine;
}
public void run() {
// Do what commandLine says to do
}
}We can parse the command string either in the constructor or at the start of the run() method.
The main program loop reads a command line, breaks it up into commands, runs all of the commands concurrently (each in a separate thread), and waits for them to all finish before issuing the next prompt. In outline, it may look like this.
for (;;) {
out.print("% "); out.flush();
String line = inputStream.readLine();
int numberOfCommands = // count how many commands there are on the
line
Thread t[] = new Thread[numberOfCommands];
for (int i=0; i<numberOfCommands; i++) {
String c = // next command on the line
t[i] = new Thread(new Command(c));
t[i].start();
}
for (int i=0; i<numberOfCommands; i++) {
t[i].join();
}
}
This main loop is in the main() method of our main class. It is not necessary for that class to implement Runnable.
Although we won't need it for project 1, the next project will require to to synchronize threads with each other. There are two reasons why we need to do this: to prevent threads from interfering with each other, and to allow them to cooperate. We use synchronized methods to prevent interference, and the built-in methods Object.wait() , Object.notify() , Object.notifyAll() , and Thread.yield() to support cooperation.
Any method can be preceded by the word synchronized (as well as public, static, etc.). The rule is: No two threads may be executing synchronized methods of the same object at the same time.
The Java system enforces this rule by associating a monitor lock with each object. When a thread calls a synchronized method of an object, it tries to grab the object's monitor lock. If another thread is holding the lock, it waits until that thread releases it. A thread releases the monitor lock when it leaves the synchronized method. If one synchronized method of a calls contains a call to another, a thread may have the same lock "multiple times." Java keeps track of that correctly. For example,
class C {
public synchronized void f() {
// ...
g();
// ...
}
public synchronized void g() { /* ... */ }
}
Although we won't need it for project 1, the next project will require to to synchronize threads with each other. There are two reasons why we need to do this: to prevent threads from interfering with each other, and to allow them to cooperate. We use synchronized methods to prevent interference, and the built-in methods Object.wait() , Object.notify() , Object.notifyAll() , and Thread.yield() to support cooperation.
Any method can be preceded by the word synchronized (as well as public, static, etc.). The rule is: No two threads may be executing synchronized methods of the same object at the same time.
The Java system enforces this rule by associating a monitor lock with each object. When a thread calls a synchronized method of an object, it tries to grab the object's monitor lock. If another thread is holding the lock, it waits until that thread releases it. A thread releases the monitor lock when it leaves the synchronized method. If one synchronized method of a calls contains a call to another, a thread may have the same lock "multiple times." Java keeps track of that correctly. For example,
class C {
public synchronized void f() {
// ...
g();
// ...
}
public synchronized void g() { /* ... */ }
}
If a thread calls C.g() "from the outside", it grabs the lock before executing the body of g() and releases it when done. If it calls C.f(), it grabs the lock on entry to f(), calls g() without waiting, and only releases the lock on returning from f().
Sometimes a thread needs to wait for another thread to do something before it can continue. The methods wait() and notify(), which are defined in class Object and thus inherited by all classes, are made for this purpose. They can only be called from within synchronized methods. A call to wait() releases the monitor lock and puts the calling thread to sleep (i.e., it stops running). A subsequent call to notify on the same object wakes up a sleeping thread and lets it start running again. If more than one thread is sleeping, one is chosen arbitrarily2 and if no threads are sleeping in this object, notify() does nothing. The awakened thread has to wait for the monitor lock before it starts; it competes on an equal basis with other threads trying to get into
the monitor. The method notifyAll is similar, but wakes up all threads sleeping in the object.
class Buffer {
private List queue = new ArrayList();
public synchronized void put(Object o) {
queue.add(o);
notify();
}
public synchronized Object get() {
while (queue.isEmpty()) {
wait();
}
return queue.remove(0);
}
}This class solves the so-call "producer-consumer" problem. (The class ArrayList and interface List are part of the java.util package.) "Producer" threads somehow create objects and put them into the buffer by calling Buffer.put(), while "consumer" threads remove objects from the buffer (using Buffer.get()) and do something with them. The problem is that a consumer thread may call Buffer.get() only to discover that the queue is empty. By calling wait() it releases the monitor lock and goes to sleep so that producer threads can call put() to add more objects. Each time a producer adds an object, it calls notify() just in case there is some consumer waiting for an object.
This example is not correct as it stands (and the Java compiler will reject it). The wait() method can throw an InterruptedException exception, so the get() method must either catch it or declare that it throws InterruptedException as well. The simplest solution is just to catch the exception and print it:
class Buffer {
private List queue = new ArrayList();
public synchronized void put(Object o) {
queue.add(o);
notify();
}
public synchronized Object get() {
while (queue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return queue.remove(0);
}
}
Sometimes a thread needs to wait for another thread to do something before it can continue. The methods wait() and notify(), which are defined in class Object and thus inherited by all classes, are made for this purpose. They can only be called from within synchronized methods. A call to wait() releases the monitor lock and puts the calling thread to sleep (i.e., it stops running). A subsequent call to notify on the same object wakes up a sleeping thread and lets it start running again. If more than one thread is sleeping, one is chosen arbitrarily2 and if no threads are sleeping in this object, notify() does nothing. The awakened thread has to wait for the monitor lock before it starts; it competes on an equal basis with other threads trying to get into
the monitor. The method notifyAll is similar, but wakes up all threads sleeping in the object.
class Buffer {
private List queue = new ArrayList();
public synchronized void put(Object o) {
queue.add(o);
notify();
}
public synchronized Object get() {
while (queue.isEmpty()) {
wait();
}
return queue.remove(0);
}
}This class solves the so-call "producer-consumer" problem. (The class ArrayList and interface List are part of the java.util package.) "Producer" threads somehow create objects and put them into the buffer by calling Buffer.put(), while "consumer" threads remove objects from the buffer (using Buffer.get()) and do something with them. The problem is that a consumer thread may call Buffer.get() only to discover that the queue is empty. By calling wait() it releases the monitor lock and goes to sleep so that producer threads can call put() to add more objects. Each time a producer adds an object, it calls notify() just in case there is some consumer waiting for an object.
This example is not correct as it stands (and the Java compiler will reject it). The wait() method can throw an InterruptedException exception, so the get() method must either catch it or declare that it throws InterruptedException as well. The simplest solution is just to catch the exception and print it:
class Buffer {
private List queue = new ArrayList();
public synchronized void put(Object o) {
queue.add(o);
notify();
}
public synchronized Object get() {
while (queue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return queue.remove(0);
}
}
The method printStackTrace() prints some information about the exception, including the line number where it happened. It is a handy thing to put in a catch clause if we don't know what else to put there. Never use an empty catch clause. If we violate this rule, we will live to regret it!
There is also a version of Object.wait() that takes an integer parameter. The call wait(n) will return after n milliseconds if nobody wakes up the thread with notify or notifyAll sooner.
We may wonder why Buffer.get() uses while (queue.isEmpty()) rather than if (queue.isEmpty()).
In this particular case, either would work. However, in more complicated situations, a sleeping thread might be awakened for the "wrong" reason. Thus it is always a good idea when we wake up to recheck the condition that made to decide to go to sleep before we continue.
There is also a version of Object.wait() that takes an integer parameter. The call wait(n) will return after n milliseconds if nobody wakes up the thread with notify or notifyAll sooner.
We may wonder why Buffer.get() uses while (queue.isEmpty()) rather than if (queue.isEmpty()).
In this particular case, either would work. However, in more complicated situations, a sleeping thread might be awakened for the "wrong" reason. Thus it is always a good idea when we wake up to recheck the condition that made to decide to go to sleep before we continue.
No comments:
Post a Comment