We’ll conclude our examples with an examination of job scheduling. Unlike round-robin scheduling, job scheduling is not related to thread starvation prevention or fairness. The concept of job scheduling is more closely related to when a runnable object is executed than to how a runnable object is run.
There are many applications of job scheduling. We could have a word
processor application that needs to save work every five minutes to
prevent data loss. We could have a backup program that needs to do an
incremental backup every day; this same program may also need to do a
full backup once a week. In our Animate applet (see Chapter 2), we needed to generate a repaint request
every second. At the time, we accomplished that by having the timer
thread schedule itself by calling the sleep()
method repeatedly. In that example, the scheduling of the repaint
request was simple to implement, and we only had this single repeated
job to schedule.
For more complex scheduling of jobs, or for programs that have countless jobs that need to be scheduled, having a dedicated job scheduler may be easier than implementing the scheduling of every job in the program. Furthermore, in the case of the timer thread, we needed to create a thread just to handle the job. If many jobs are required, a job scheduler may be preferred over having many threads that schedule themselves. This dedicated job scheduler can run all the jobs in its own thread, or it can assign the jobs to a thread pool to better use the thread resources of the underlying platform.
Here’s an implementation of a job scheduler class:
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); } }
Surprisingly, the implementation of a job scheduler is fairly simple:
we just need to iterate over the requested jobs (the elements of the
jobs
vector) and either add the jobs that need
to be executed to a thread pool for processing or start 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. This 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. Hence, after a job is executed, we need to calculate whether another execution is necessary and when to perform this execution.
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 programmer.
There are eight methods provided to the programmer in our JobScheduler class:
Used for jobs that are executed once; simply runs the job.
Used for jobs that are executed once; runs the job after the specified number of milliseconds have elapsed.
Used for jobs that are executed once; runs the job at the time specified.
Used for repeating jobs. These methods run the job after the number
of milliseconds specified by the millis
parameter has elapsed. Then 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 will be 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.
Used for repeating jobs. These methods run the job at the time
specified, then run the job again after the specified number of
milliseconds has elapsed. This process is repeated as specified by
the count
parameter. If no count is specified,
the job will be repeated forever.
These methods also support the HOURLY
,
DAILY
, WEEKLY
,
MONTHLY
, and YEARLY
constants.
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 will remove the first
job that it finds on the list.
As rich as this set of methods is, it can be considered weak 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.
Criteria for jobs are often defined this way. We do not think of a backup as running on a particular day and time, but on a particular day of the week (e.g., every Sunday at 2:00 A.M.). Paychecks are issued on the 1st and 15th day of the month. Vacation time-shares are assigned by the week in the year. With design requirements that are modeled from the real world, the job scheduler may have to be modified to support these requirements.
The task of enhancing the job scheduler for these cases is left as an exercise for the reader. However, this is not very difficult to accomplish, given the availability of the Calendar class. For example, with this class, we can easily develop the enhancement for executing a job at a certain day of the week, starting from a particular day:
public void executeAtNextDOW(Runnable job, Date when, int DOW) { Calendar target = Calendar.getInstance(); target.setTime(when); while (target.get(Calendar.DAY_OF_WEEK) != DOW) target.add(Calendar.DATE, 1); executeAt(job, target.getTime()); }
With this enhancement, we can now execute a job on Sunday like this:
executeAtNextDOW(job, new Date(), Calendar.SUNDAY);
Should the job scheduler be implemented by using a daemon thread? At first glance, this seems like a good choice. After all, if there are no user threads, then there are no jobs to be scheduled. The problem is that there may be jobs on the vector that are already scheduled and are waiting to be executed. Since these jobs do not schedule themselves, there are no threads assigned to them while they wait on the vector. It is therefore possible for all user threads to exit while there are still jobs to be scheduled. In this situation, if the job scheduler was configured as a daemon thread, it would exit with jobs still waiting to be executed.
By using the DaemonLock class that we developed in Chapter 6, we can do a little better: we can make the job scheduler a daemon thread, and we can ensure that it will exit only when there are no more jobs to schedule and there are no other user threads running. All we need to do is acquire the daemon lock when jobs are added to the scheduler, and release the daemon lock when jobs are removed from the scheduler. This only works when the job scheduler is constructed without a thread pool (that is, when each job will be run in a new thread), since the thread pool threads are not daemon threads.