Consider this revision to the Animate example:
import java.applet.Applet; public class Animate extends Applet { TimerThread t; public void start() { if (t == null) t = new TimerThread(this, 500); t.start(); } public void stop() { t.shouldRun = false; try { t.join(); } catch (InterruptedException e) {} // t = null; } }
In our last version of the Animate applet (see Section 2.3,” earlier in this chapter), the
start()
method of the applet created a new
TimerThread object and started it. But what if we had only created
the TimerThread once? In the example just shown, we once again create
a new TimerThread in the start()
method of the
applet; however, since we know the thread will be stopped in the
stop()
method, we try to restart the stopped
thread in the start()
method. In other words, we
create the TimerThread only once and use this one thread object to
start and stop the animation. By starting and stopping a single
TimerThread, we do not need to create a new instance of TimerThread
every time the applet is started, and the garbage collector will not
need to clean up the TimerThread instance that’s left when the
applet is stopped and the TimerThread dereferenced.
But will this
work?
Unfortunately, the answer is no. It turns out that when a thread is
stopped, the state of the thread object is set so that it is not
restartable. In our case, when we try to restart the thread by
calling the TimerThread’s start()
method,
nothing happens. The start()
method won’t
return an exception condition, but the run()
method also won’t be called. The isAlive()
method also won’t return true
. In other
words, never restart a thread. An instance of a thread object should
be used once and only once.
Can an already stopped thread be
stopped?
At first glance, this may
seem an odd question. But the answer is yes, and the reason is that
it avoids a race condition that would occur
otherwise. We know there are two ways a thread can be stopped, so you
could stop a thread that has already exited because its
run()
method terminated normally. If the Thread
class did not allow the stop()
method to be
called on a stopped thread, this would require us to check if the
thread was still running before we stopped it, and we’d have to
avoid a race condition in which the run()
method
could terminate in between the time when we checked if the thread was
alive and when we called the stop()
method. This
would be a big burden on the Java developer, so, instead, the
stop()
method can be called on a thread that has
already stopped.
What happens when we call the join() method for a thread
that was stopped a long time ago?
In the examples so far, we assumed the usage of the
join()
method was to wait for a thread to
complete or to stop. But this assumption is not necessary; if the
thread is already stopped, it will return immediately. This may seem
obvious, but it should be noted that a race condition would have
resulted if the join()
method had required that
the thread be alive when the method was first called.
What would be the best way to join() with more than one thread? Let’s look at the following code:
import java.applet.Applet; public class MyJoinApplet extends Applet { Thread t[] = new Thread[30]; public void start() { for (int i=0; i<30; i++) { t[i] = new CalcThread(i); t[i].start(); } } public void stop() { for (int i=0; i<30; i++) { try { t[i].join(); } catch (InterruptedException e) {} } } }
In this example, we start 30 CalcThread objects. We have not actually
defined the CalcThread class, but for this example, we assume it is a
class that is used to calculate part of a large mathematical
algorithm. In the applet’s stop()
method,
we execute a loop waiting for all the started threads to be finished.
Is this the best way to wait for more than one thread? Since it is
possible to join()
with an already stopped
thread, it is perfectly okay to join()
with a
group of threads in a loop, even if the threads finish in an order
different than the order in which they were started. No matter how we
might have coded the join()
loop, the time to
complete the join()
will be the time it takes
for the last thread to finish.
Of course, there may be cases where a specific joining mechanism is desired, but this depends on details other than the threading system. There is no performance penalty to pay for joining in an order that is not the order of completion.