Appendix A. Superseded Threading Utilities

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.

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.