Readers of previous editions of this book will have noticed that many of the classes we developed for those editions have been replaced. The reason has to do with the many new classes provided by J2SE 5.0. Prior to J2SE 5.0, developers were left to create or purchase a library that provided the high-level threading support needed by more complex programs. While these libraries can still be used, it is recommended that programs migrate to the core J2SE 5.0 classes since that leaves one less library to maintain, test, and download during execution.
While the examples in the previous edition of this book are now obsolete, there are a few advantages to including them in this appendix (and in the online source). The examples were designed to teach the subject of threading. They were designed to be simplistic, not loaded with features, and specifically target a particular subject. Most of those subjects are now discussed in relation to the new classes in J2SE 5.0, and the rest of them are no longer necessary since we are no longer maintaining our own library. Still, for research purposes, there is advantage in examining them.
As this book goes to press, J2SE 5.0 is only a beta release, so many developers cannot yet use the new classes in J2SE 5.0. Those developers will also find these classes useful.
So for those who may be interested, here is a quick review of our obsolete classes. Obviously, learning the examples in this appendix is optional. Using these tools should be considered only if you must use a virtual machine earlier than J2SE 5.0.
We’ll start with a BusyFlag
class:
package javathreads.examples.appa; public class BusyFlag { protected Thread busyflag = null; protected int busycount = 0; public synchronized void getBusyFlag( ) { while (tryGetBusyFlag( ) == false) { try { wait( ); } catch (Exception e) {} } } public synchronized boolean tryGetBusyFlag( ) { if (busyflag == null) { busyflag = Thread.currentThread( ); busycount = 1; return true; } if (busyflag == Thread.currentThread( )) { busycount++; return true; } return false; } public synchronized void freeBusyFlag( ) { if (getBusyFlagOwner( ) == Thread.currentThread( )) { busycount--; if (busycount == 0) { busyflag = null; notify( ); } } } public synchronized Thread getBusyFlagOwner( ) { return busyflag; } }
The BusyFlag
class implements a
basic, no-frills, mutually exclusive lock. It also allows the locks to
be nested—the owner thread can lock the busy flag multiple times. It is
much simpler than the ReentrantLock
class. There is no internal support for condition variables.
There is no support for timeouts. There is no concept of fairness in
granting the busy flag. And our implementation does not attempt to
minimize synchronization.
Simplistically, the purpose of this class is to use Java’s basic synchronization mechanism to achieve, well, synchronization. This allows the program to lock at any scope or for any purpose.
The BusyFlag
class contains
four methods. The tryGetBusyFlag()
class is used to obtain a lock (a.k.a. the busyflag
). It grabs the busy flag if it is
available while returning false
if
the flag is already owned by another thread. It also allows nested locks
by incrementing a counter if the current thread already owns the flag.
The synchronized
keyword is used to
protect against race conditions while grabbing this flag.
The getBusyFlag()
method uses
the tryGetBusyFlag()
method to
repeatedly try grabbing the flag until it is successful. If the flag is
not available, it uses the wait-and-notify mechanism to wait for the
flag to be returned. The freeBusyFlag()
method decrements the counter.
And if the counter is zero, this method declares that the flag has no
owner and notifies any threads that are waiting to grab the flag.
The getBusyFlagOwner()
method
is merely an administration method that allows a thread to determine who
is the owner of the busy flag. Also note that due to a race condition,
the result that is returned is only guaranteed not to change if the
current thread is returned as the owner of the busy flag.
Here is an implementation of the CondVar
class:
package javathreads.examples.appa; public class CondVar { private BusyFlag SyncVar; public CondVar( ) { this(new BusyFlag( )); } public CondVar(BusyFlag sv) { SyncVar = sv; } public void cvWait( ) throws InterruptedException { cvTimedWait(SyncVar, 0); } public void cvWait(BusyFlag sv) throws InterruptedException { cvTimedWait(sv, 0); } public void cvTimedWait(int millis) throws InterruptedException { cvTimedWait(SyncVar, millis); } public void cvTimedWait(BusyFlag sv, int millis) throws InterruptedException { int i = 0; InterruptedException errex = null; synchronized (this) { // You must own the lock in order to use this method if (sv.getBusyFlagOwner( ) != Thread.currentThread( )) { throw new IllegalMonitorStateException( "current thread not owner"); } // Release the lock (Completely) while (sv.getBusyFlagOwner( ) == Thread.currentThread( )) { i++; sv.freeBusyFlag( ); } // Use wait( ) method try { if (millis == 0) { wait( ); } else { wait(millis); } } catch (InterruptedException iex) { errex = iex; } } // Obtain the lock (Return to original state) for (; i>0; i--) { sv.getBusyFlag( ); } if (errex != null) throw errex; return; } public void cvSignal( ) { cvSignal(SyncVar); } public synchronized void cvSignal(BusyFlag sv) { // You must own the lock in order to use this method if (sv.getBusyFlagOwner( ) != Thread.currentThread( )) { throw new IllegalMonitorStateException( "current thread not owner"); } notify( ); } public void cvBroadcast( ) { cvBroadcast(SyncVar); } public synchronized void cvBroadcast(BusyFlag sv) { // You must own the lock in order to use this method if (sv.getBusyFlagOwner( ) != Thread.currentThread( )) { throw new IllegalMonitorStateException( "current thread not owner"); } notifyAll( ); } }
The CondVar
class implements a
basic condition variable for use with the BusyFlag
class. There is no concept of
fairness in notification. It is constructed separately from the BusyFlag
class—as compared to Condition
objects, which are generated from
the Lock
class via the newCondition()
method. And like the BusyFlag
class, the implementation doesn’t
attempt to minimize synchronization.
The purpose of this class is to allow Java’s wait-and-notify
mechanism to work with explicit locking (locks at any scope). This
allows the program to have condition variable support for the BusyFlag
class. It also allows a single lock
to have more than one condition variable, where the wait-and-notify
mechanism needs a separate object for every type of notification.
The CondVar
class provides four
methods for waiting for notification; three of these methods can be
considered convenience methods. The primary method is the cvTimedWait()
method. This method frees the
ownership of the busy flag completely and then uses the standard
wait()
method to perform the wait. If
the time to wait is zero, this method waits indefinitely for the
notification. Otherwise, it uses the timeout specified. Upon returning,
it grabs the lock (note that it must do that as many times as the lock
was released to support the nesting semantics of our BusyFlag
class). Also note that it may still
wait upon receiving notification as it can still block while reacquiring
the flag. In fact, that’s the case with all notification-based
techniques (the Condition
class, the
wait-and-notify mechanism); it’s just in this code that you see the
effect explicitly.
Two of the convenience methods allow the program to specify a
timeout or wait indefinitely. The last one allows you to specify an
alternate busy flag class—a flag that is different from the one
specified during construction. Specifying an alternate busy flag is not
a feature supported by the Condition
class—a Condition
instance is tightly
bound to the Lock
instance from which
it was obtained. This feature allows notification between two groups of
threads that are operating on different locks. In terms of
functionality, this is a minor enhancement for a very rare need. Using
the Condition
class, a common
Lock
object could be created just for
notification between the two groups of threads to achieve the same
thing.
The cvSignal()
method is used
to send a single notification—using the notify()
method. As with the wait methods, it
is overloaded to allow the program to specify an alternate busy flag.
The cvBroadcast()
method is used to
send notifications to all the waiting threads—using the notifyAll()
method. It, too, is overloaded to
allow the program to specify an alternate busy flag.
Here is an implementation of the Barrier
class:
package javathreads.examples.appa; public class Barrier { private int threads2Wait4; private InterruptedException iex; public Barrier (int nThreads) { threads2Wait4 = nThreads; } public synchronized int waitForRest( ) throws InterruptedException { int threadNum = --threads2Wait4; if (iex != null) throw iex; if (threads2Wait4 <= 0) { notifyAll( ); return threadNum; } while (threads2Wait4 > 0) { if (iex != null) throw iex; try { wait( ); } catch (InterruptedException ex) { iex = ex; notifyAll( ); } } return threadNum; } public synchronized void freeAll( ) { iex = new InterruptedException("Barrier Released by freeAll"); notifyAll( ); } }
The Barrier
class is a basic,
no-frills implementation of a barrier. Implementation of the Barrier
class with the basic synchronization
techniques is straightforward. We simply have each thread that arrives
at the barrier (i.e., that calls the waitForRest()
method) call the wait()
method while the last thread to arrive
at the barrier has the task of notifying all of the waiting threads. If
any of the threads receives an interruption, all of the threads receive
the same interruption. Another method, freeAll()
, is also provided to generate an
interrupt on all of the threads. As an added benefit, a thread number is
assigned to the threads to help distinguish the waiting threads. The
last thread to reach the barrier is assigned the value of zero, and any
thread that reaches the barrier after the barrier has been released is
assigned a negative value. This indicates an error condition for the
thread.
This implementation of the barrier is a single-use implementation. Once the barrier reaches the thread limit as specified by the constructor, or an error is generated, the barrier no longer blocks any threads.
Here is an implementation of the RWLock
(reader/writer
lock) class:
package javathreads.examples.appa; import java.util.*; class RWNode { static final int READER = 0; static final int WRITER = 1; Thread t; int state; int nAcquires; RWNode(Thread t, int state) { this.t = t; this.state = state; nAcquires = 0; } } public class RWLock { private Vector waiters; private int firstWriter( ) { Enumeration e; int index; for (index = 0, e = waiters.elements( ); e.hasMoreElements( ); index++) { RWNode node = (RWNode) e.nextElement( ); if (node.state == RWNode.WRITER) return index; } return Integer.MAX_VALUE; } private int getIndex(Thread t) { Enumeration e; int index; for (index = 0, e = waiters.elements( ); e.hasMoreElements( ); index++) { RWNode node = (RWNode) e.nextElement( ); if (node.t == t) return index; } return -1; } public RWLock( ) { waiters = new Vector( ); } public synchronized void lockRead( ) { RWNode node; Thread me = Thread.currentThread( ); int index = getIndex(me); if (index == -1) { node = new RWNode(me, RWNode.READER); waiters.addElement(node); } else node = (RWNode) waiters.elementAt(index); while (getIndex(me) > firstWriter( )) { try { wait( ); } catch (Exception e) {} } node.nAcquires++; } public synchronized void lockWrite( ) { RWNode node; Thread me = Thread.currentThread( ); int index = getIndex(me); if (index == -1) { node = new RWNode(me, RWNode.WRITER); waiters.addElement(node); } else { node = (RWNode) waiters.elementAt(index); if (node.state == RWNode.READER) throw new IllegalArgumentException("Upgrade lock"); node.state = RWNode.WRITER; } while (getIndex(me) != 0) { try { wait( ); } catch (Exception e) {} } node.nAcquires++; } public synchronized void unlock( ) { RWNode node; Thread me = Thread.currentThread( ); int index; index = getIndex(me); if (index > firstWriter( )) throw new IllegalArgumentException("Lock not held"); node = (RWNode) waiters.elementAt(index); node.nAcquires--; if (node.nAcquires == 0) { waiters.removeElementAt(index); notifyAll( ); } } }
The RWLock
class implements a
basic reader-writer lock. As with Java’s ReentrantReadWriteLock
class, this class is
implemented in a way to prevent lock starvation.
The interface to the reader-writer lock is very simple: there’s a
lockRead()
method to acquire the read
lock, a lockWrite()
method to acquire
the write lock, and an unlock()
method to release the lock. (only a single unlock()
method is required, for reasons we’ll
explore in a moment). Threads that are attempting to acquire the lock
are held in a waiters
vector. This is
to allow the RWLock
class to order
the requests for the purpose of preventing lock starvation. Furthermore,
the Vector
class is used, instead of
the more recent container classes, in order to allow the reader-writer
lock to be used with older versions of Java.
Because we need to keep track of how each thread wants to acquire
the lock—whether it wants to acquire the read lock or the write lock—we
need to create a class to encapsulate the information of the thread that
made the request and the type of request it made. This is the RWNode
class; our waiters
vector holds elements of type RWNode
.
Acquisition of the read lock is done in an orderly manner—the
RWLock
class doesn’t just grant the
read lock because another thread is also holding the read lock. In order
to obtain the read lock, a thread that wants the write lock must not
already be in the queue. If the nodes that are ahead of the current
thread in the waiters
queue want only
to acquire the read lock, we can go ahead and acquire the lock.
Otherwise, we must wait until all of the nodes that want to acquire the
write lock—and are ahead in the waiter
vector—acquire and ultimately free the
lock.
Acquisition of the write lock is stricter: we must be in position zero in the vector. Only one thread may hold the write lock at a time.
This class also supports nested locks. This is accomplished by
keeping track of the number of acquisitions requested. Since the read
lock can be granted to multiple threads simultaneously, we can no longer
use a simple instance variable (as we did in the BusyFlag
class); we must associate the
nAcquires
count with each particular
thread. Both acquisition methods must check to see if there is already a
node associated with the calling thread.
This reader-writer lock class does not have the notion of
“upgrading” the lock; that is, if you hold the reader lock, you can’t
acquire the writer lock. You must explicitly release the reader lock
before you attempt to acquire the writer lock, or you receive an
IllegalArgumentException
. If an
upgrade feature were provided, the class itself would also have to
release the reader lock before acquiring the writer lock. A true upgrade
is not possible due to writer lock requests or possible upgrades
requests from threads that are also holding reader locks.
Finally, the reader-writer lock class contains some methods to
search the waiters
vector for the
first node in the queue that represents a thread attempting to acquire
the write lock (the firstWriter()
method) and to find the index in the vector of the node associated with
the calling thread (the getIndex()
method). We can’t use the indexOf()
method of the Vector
class for this
purpose because we’d have to pass the indexOf()
method an object of type RWNode
, but all we have is a Thread
object.[1]
Here is an implementation of the ThreadPool
class:
package javathreads.examples.appa; import java.util.*; public class ThreadPool { class ThreadPoolRequest { Runnable target; Object lock; ThreadPoolRequest(Runnable t, Object l) { target = t; lock = l; } } class ThreadPoolThread extends Thread { ThreadPool parent; boolean shouldRun = true; ThreadPoolThread(ThreadPool parent, int i) { super("ThreadPoolThread " + i); this.parent = parent; } public void run( ) { ThreadPoolRequest obj = null; while (shouldRun) { try { parent.cvFlag.getBusyFlag( ); while (obj == null && shouldRun) { try { obj = (ThreadPoolRequest) parent.objects.elementAt(0); parent.objects.removeElementAt(0); } catch (ArrayIndexOutOfBoundsException aiobe) { obj = null; } catch (ClassCastException cce) { System.err.println("Unexpected data"); obj = null; } if (obj == null) { try { parent.cvAvailable.cvWait( ); } catch (InterruptedException ie) { return; } } } } finally { parent.cvFlag.freeBusyFlag( ); } if (!shouldRun) return; obj.target.run( ); try { parent.cvFlag.getBusyFlag( ); nObjects--; if (nObjects == 0) parent.cvEmpty.cvSignal( ); } finally { parent.cvFlag.freeBusyFlag( ); } if (obj.lock != null) { synchronized(obj.lock) { obj.lock.notify( ); } } obj = null; } } } Vector objects; int nObjects = 0; CondVar cvAvailable, cvEmpty; BusyFlag cvFlag; ThreadPoolThread poolThreads[]; boolean terminated = false; public ThreadPool(int n) { cvFlag = new BusyFlag( ); cvAvailable = new CondVar(cvFlag); cvEmpty = new CondVar(cvFlag); objects = new Vector( ); poolThreads = new ThreadPoolThread[n]; for (int i = 0; i < n; i++) { poolThreads[i] = new ThreadPoolThread(this, i); poolThreads[i].start( ); } } private void add(Runnable target, Object lock) { try { cvFlag.getBusyFlag( ); if (terminated) throw new IllegalStateException("Thread pool has shutdown"); objects.addElement(new ThreadPoolRequest(target, lock)); nObjects++; cvAvailable.cvSignal( ); } finally { cvFlag.freeBusyFlag( ); } } public void addRequest(Runnable target) { add(target, null); } public void addRequestAndWait(Runnable target) throws InterruptedException { Object lock = new Object( ); synchronized(lock) { add(target, lock); lock.wait( ); } } public void waitForAll(boolean terminate) throws InterruptedException { try { cvFlag.getBusyFlag( ); while (nObjects != 0) cvEmpty.cvWait( ); if (terminate) { for (int i = 0; i < poolThreads.length; i++) poolThreads[i].shouldRun = false; cvAvailable.cvBroadcast( ); terminated = true; } } finally { cvFlag.freeBusyFlag( ); } } public void waitForAll( ) throws InterruptedException { waitForAll(false); } }
The ThreadPool
class implements
a thread pool—similar to the thread pool executor discussed in Chapter 10. The inner class in this
example performs most of the work. Each thread waits for work; when it
is signaled, it simply pulls the first object from the vector and
executes the object. When execution of that object is finished, the
thread must notify the lock associated with the object (if any) so that
the addRequestAndWait()
method knows
when to return; the thread must also notify the thread pool itself so
that the waitForAll()
method checks
to see if it is time for it to return.
As a result, this code has three waiting points :
Some request objects have an associated lock object (the
Object
created in the addRequestAndWait()
method). The addRequestAndWait()
method uses the
standard wait and notify technique to wait on this object; it
receives notification after the run()
method has been executed by one of
the ThreadPoolThread
objects.
A CondVar
object (i.e., a
condition variable), cvAvailable
,
is associated with the cvBusyFlag
. This condition is used to
signal that work is available to be performed. Whenever the nObjects
variable is incremented, work is
available, so the add()
method
signals a thread that a new object is available. Similarly, when
there are no objects in the vector to be processed, the ThreadPoolThread
objects wait on that
condition variable.
A CondVar
object, cvEmpty
, is also associated with the same
cvBusyFlag
. This condition is
used to signal that all pending work has been completed—that is,
that the nObjects
variable has
reached zero. The waitForAll()
method waits for this condition, which is signaled by a ThreadPoolThread
when it sets nObjects
to zero.
We use condition variables for the last two cases because they
share the same lock (the cvBusyFlag
,
which protects access to nObjects
)
even though they have different values for their condition. If we had
used the standard wait-and-notify mechanism to signal the threads that
are interested in the value of nObjects
, we could not have controlled
notification as well: whenever nObjects
was set to zero, we’d have to notify
all ThreadPoolThreads
as well as
notifying the thread that is executing the waitForAll()
method.
Note that objects that are to be run by the thread pool are
expected to implement the Runnable
interface. This is similar to the thread pool executor. This doesn’t
mean that a new thread is created for each task. This interface allows
us to take existing code that uses threads and run those tasks via a
thread pool instead.
Interestingly enough, there is no way to shut down a thread pool
automatically. If the thread pool object were to go out of scope, it
would never be garbage collected. The thread pool thread objects (like
all thread objects) are held in an internal data structure within the
virtual machine, so they are not garbage collected until they exit. And
because they have a reference to the thread pool itself, the thread pool
cannot be garbage collected until the thread pool threads are garbage
collected. So we have to have some way of signaling the thread pool to
exit: we do that by passing a true
parameter to the waitForAll()
method.
Then, when the thread pool has run all of its jobs, the waitForAll()
method arranges for the thread
pool threads to terminate and marks the thread pool so that no more jobs
can be added to it. The thread pool threads then exit, and the thread
pool can be garbage collected.
Here is an implementation of the JobScheduler
class to execute a task:
package javathreads.examples.appa; import java.util.*; public class JobScheduler implements Runnable { final public static int ONCE = 1; final public static int FOREVER = -1; final public static long HOURLY = (long)60*60*1000; final public static long DAILY = 24*HOURLY; final public static long WEEKLY = 7*DAILY; final public static long MONTHLY = -1; final public static long YEARLY = -2; private class JobNode { public Runnable job; public Date executeAt; public long interval; public int count; } private ThreadPool tp; private DaemonLock dlock = new DaemonLock( ); private Vector jobs = new Vector(100); public JobScheduler(int poolSize) { tp = (poolSize > 0) ? new ThreadPool(poolSize) : null; Thread js = new Thread(this); js.setDaemon(true); js.start( ); } private synchronized void addJob(JobNode job) { dlock.acquire( ); jobs.addElement(job); notify( ); } private synchronized void deleteJob(Runnable job) { for (int i=0; i < jobs.size( ); i++) { if (((JobNode) jobs.elementAt(i)).job == job) { jobs.removeElementAt(i); dlock.release( ); break; } } } private JobNode updateJobNode(JobNode jn) { Calendar cal = Calendar.getInstance( ); cal.setTime(jn.executeAt); if (jn.interval == MONTHLY) { // There is a minor bug. (see java.util.calendar) cal.add(Calendar.MONTH, 1); jn.executeAt = cal.getTime( ); } else if (jn.interval == YEARLY) { cal.add(Calendar.YEAR, 1); jn.executeAt = cal.getTime( ); } else { jn.executeAt = new Date(jn.executeAt.getTime( ) + jn.interval); } jn.count = (jn.count == FOREVER) ? FOREVER : jn.count -1; return (jn.count != 0) ? jn : null; } private synchronized long runJobs( ) { long minDiff = Long.MAX_VALUE; long now = System.currentTimeMillis( ); for (int i=0; i < jobs.size( );) { JobNode jn = (JobNode) jobs.elementAt(i); if (jn.executeAt.getTime( ) <= now) { if (tp != null) { tp.addRequest(jn.job); } else { Thread jt = new Thread(jn.job); jt.setDaemon(false); jt.start( ); } if (updateJobNode(jn) == null) { jobs.removeElementAt(i); dlock.release( ); } } else { long diff = jn.executeAt.getTime( ) - now; minDiff = Math.min(diff, minDiff); i++; } } return minDiff; } public synchronized void run( ) { while (true) { long waitTime = runJobs( ); try { wait(waitTime); } catch (Exception e) {}; } } public void execute(Runnable job) { executeIn(job, (long)0); } public void executeIn(Runnable job, long millis) { executeInAndRepeat(job, millis, 1000, ONCE); } public void executeInAndRepeat(Runnable job, long millis, long repeat) { executeInAndRepeat(job, millis, repeat, FOREVER); } public void executeInAndRepeat(Runnable job, long millis, long repeat, int count) { Date when = new Date(System.currentTimeMillis( ) + millis); executeAtAndRepeat(job, when, repeat, count); } public void executeAt(Runnable job, Date when) { executeAtAndRepeat(job, when, 1000, ONCE); } public void executeAtAndRepeat(Runnable job, Date when, long repeat) { executeAtAndRepeat(job, when, repeat, FOREVER); } public void executeAtAndRepeat(Runnable job, Date when, long repeat, int count) { JobNode jn = new JobNode( ); jn.job = job; jn.executeAt = when; jn.interval = repeat; jn.count = count; addJob(jn); } public void cancel(Runnable job) { deleteJob(job); } }
The JobScheduler
class
implements a time-based execution system—similar to the scheduled
executor discussed in Chapter
11. Like the ScheduledThreadPoolExecutor
class, this class
also uses a thread pool internally, allowing the tasks to execute in the
separate threads within the pool. However, this class also provides the
option not to use a thread pool, meaning that separate threads are
started for every job. This option is useful if the job is a long-term
task or for a job that runs in the background indefinitely. Assuming
that the threading system doesn’t get overloaded, this also allows the
jobs to be executed as close to the requested time as possible.
The class is designed to be as simple—and as basic—as possible:
the class just iterates over the requested jobs (the elements in the
jobs
vector) and either adds the jobs
that need to be executed to a thread pool for processing or starts a new
thread to execute the job. In addition, we need to find the time for the
job that is due to run next, and wait for this time to occur. The entire
process is then repeated.
For completeness, we’ve added a little complexity in our JobScheduler
class. In addition to accepting a
runnable object that can be executed and a time at which to perform the
job, we also accept a count of the number of times the job is to be
performed and the time to wait between executions of the job.
Consequently, after a job is executed, we need to calculate whether
another iteration is necessary and when to perform this
iteration.
In our JobScheduler
class, this
is all handled by a single thread that calls the runJobs()
method. The task of deciding whether
the job needs to be executed again is done by the updateJobNode()
method; adding jobs to and
deleting jobs from the requested jobs vector is accomplished by the
addJob()
and deleteJob()
methods, respectively. Most of the
logic for the JobScheduler
class is
actually the implementation of the many options and methods in the
interface provided for the developer.
Our JobScheduler
class provides
eight methods:
public void execute(Runnable
job)
Used for a job that is to be executed once; simply runs the job.
public void executeIn(Runnable job,
long millis)
Used for a job that is to be executed once; runs the job after the specified number of milliseconds has elapsed.
public void executeAt(Runnable job,
Date when)
Used for a job that is to be executed once; runs the job at the time specified.
public void
executeInAndRepeat(Runnable job, long millis, long
repeat)
public void
executeInAndRepeat(Runnable job, long millis, long repeat, int
count)
public void
executeAtAndRepeat(Runnable job, Date when, long
repeat)
public void
executeAtAndRepeat(Runnable job, Date when, long repeat, int
count)
Used for repeating jobs. These methods run the job after the
number of milliseconds specified by the millis
parameter has elapsed (or at the
time specified by the when
parameter). They run the job again after the number of
milliseconds specified by the repeat
parameter has elapsed. This
process is repeated as specified by the count
parameter. If no count is
specified, the job is repeated forever.
The constants HOURLY
,
DAILY
, WEEKLY
, MONTHLY
, and YEARLY
may also be passed as the
repeat
parameter. The HOURLY
, DAILY
, and WEEKLY
parameters are provided for
convenience. However, the MONTHLY
and YEARLY
parameters are processed
differently by the job scheduler since the scheduler has to take
into account the different number of days in the month and the
leap year.
public void cancel(Runnable
job)
Cancels the specified job. No error is generated if the job
is not in the requested jobs vector since it is possible that the
job has executed and been removed from the vector before the
cancel()
method is called. If
the same job is placed on the list more than once, this method
removes the first job that it finds on the list.
As rich as the set of methods provided by this class, it can be
considered weak in features by those who have used job schedulers
provided by some operating systems. In those systems, developers can
specify criteria such as day of the week, day of the month, week of the
year, and so on. Compared to the ScheduledThreadPoolExecutor
class, it is also
missing some of the control features for repeating jobs.
Our job scheduler class depends on the DaemonLock
class. The purpose of the DaemonLock
class is to allow the job
scheduler to shut down gracefully. The main thread should exit without
shutting down the job scheduler abruptly: if there are scheduled
tasks, we want them to complete. When the job scheduler has finished
all its tasks, we want the program to exit.
We accomplish this by making the threads in the job scheduler
daemon threads; that way they exit when no more user threads are
active. The DaemonLock
class
protects against premature exit: it makes sure that one user thread is
active as long as the job scheduler has tasks to run.
Note that the ScheduledThreadPoolExecutor
class doesn’t
need to use something like this class since its shutdown()
method accomplishes a graceful
shutdown.
The DaemonLock
class looks
like this:
package javathreads.examples.appa; public class DaemonLock implements Runnable { private int lockCount = 0; public synchronized void acquire( ) { if (lockCount++ == 0) { Thread t = new Thread(this); t.setDaemon(false); t.start( ); } } public synchronized void release( ) { if (--lockCount == 0) { notify( ); } } public synchronized void run( ) { while (lockCount != 0) { try { wait( ); } catch (InterruptedException ex) {}; } } }
In a way, this appendix is like a history lesson: we have just reviewed the major classes developed in the previous editions of this book. These classes have been superceded by the additions in J2SE 5.0. While the enhancements in J2SE 5.0 provide production quality support, they also make it more difficult for readers. The new classes are designed to be used, not to be educational tools—therefore, their code is written optimally rather than simply.
By reviewing these superceded classes, we accomplish two tasks. We provide edification by showing classes that are simpler to understand. We also provide tools that can be used by developers who have not yet upgraded to J2SE 5.0. For those developers, these classes, available in the online source for this book, could be used in the interim.
[1] In J2SE 5.0, that’s no longer a problem, since the Vector
class supports intrinsics. But in
J2SE 5.0, you’ll be using the ReadWriteLock
class anyway.