Job Scheduling

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:

public void execute(Runnable job)

Used for jobs that are executed once; simply runs the job.

public void executeIn(Runnable job, long millis)

Used for jobs that are executed once; runs the job after the specified number of milliseconds have elapsed.

public void executeAt(Runnable job, Date when)

Used for jobs that are 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)

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.

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 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.

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 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset