Quite often, you need a class to perform tasks like data processing, listening to events, or checking another class' activities during the application’s lifetime. To achieve this, you probably use threads with a set of locks and notifications. Java Thread API is well documented, but you need a great deal of code and experience to make your thread work properly and efficiently. You can avoid writing such classes from scratch every time you need them and build a more robust application by applying the framework we'll discuss in this article.
Grab the code
Download the source code for this article.
Framework for long-running tasks
The primary thing about a long-lived task is that it should somehow be kept running during the application lifetime. The right way to accomplish this is to provide a thread of execution for a particular task. You create a task as a thread or as an implementation of the java.lang.Runnable interface. If you implement Runnable, you can gain better object-oriented design and avoid the single-inheritance problems. You can also more efficiently manipulate with Runnable instances, for example, using a thread pool that usually needs a Runnable instance, not a thread, to run.
The essence of the framework is the abstract class Worker (Listing A), which implements the Runnable interface and provides the helper methods for efficient task handling. Some of the methods are fully implemented, like the run() method, but some are abstract and have to be filled by you. If you want to create a long-running class, you need only to extend the Worker class and implement several abstract methods. Let’s look at these methods in more detail.
The run() method of the Worker class is designed to continuously execute the work() method until it is stopped. The work() method can be responsible for data processing, reaction to some event, file reading or writing, SQL execution, etc. It can throw an exception, so it is a good practice to propagate it and let the run() method handle it.
The run() method has two levels of try-catch clause: outside and inside the while-loop. The first try-catch clause is meant to catch all nonprogrammed exceptions and guarantee that the run() method never exits. The second clause will catch any kind of exceptions belonging to business logic and behave accordingly. If some waiting operation takes place in the work() method (e.g., waiting on an InputStream or a Socket), it is advisable to propagate an InterruptedException. The thing to keep in mind is that the work() method does not need to have any while-loop to keep it going as long as an application runs. The Worker does this for you.
When the run() method starts, it calls the prepareWorker() which is designed to prepare all resources needed for a long-running task (Listing A). In this method call, you can, for example, establish a database connection or open a file that will be used further. It is especially good to place here some blocking operations like opening a socket, because they will be done in a separate thread and thus will not block the main thread of execution.
The opposite of the previous method is the releaseWorker() which is called when the run() method is about to exit (Listing A). Here, you can put the code to dispose of system resources used by this task or to perform other cleanup. This method is similar to java.lang.Object.finalize(), but it is explicitly called before a thread terminates.
Handling errors in the framework
Another important method is the handleError(), which takes a java.lang.Throwable as a parameter. This method is called each time an error situation occurs within the run() method. It is up to you how to implement error handling. One way is to log errors and control task termination by calling halt() method (Listing A).
The isCondition() method is used to tell whether execution of the work() method can be started, thus allowing granular control over a task. It is useful in event-triggered frameworks when execution of the work() method is pending until some condition—for example, a buffer is not empty—is fulfilled. In Worker’s implementation, the condition is checked upon a lock notification and periodically with a time interval you specify in the setTimeout() method (Listing A). If you don’t need any waiting blocks in a task, just make the isCondition() method always return true.
When to terminate
You'll also need the isRunning(), broadcast(), and halt() methods. Querying isRunning(), you can check whether a task is still running and make a decision whether to terminate it. The broadcast() method just notifies the lock object and makes a task proceed if it has been waiting on this lock. The halt() method stops a task, so the run() method will exit as soon as the next isRunning() status is checked. Because this method notifies only one lock that may block this task’s thread, it is advisable to use the same lock object when you do blocking operations within the work() method (Listing B). If you can't use the same lock object, such as when you're blocking on the java.io.InputStream.read() method, you should add explicit notification of all possible locks or add java.lang.Thread.interrupt() to your halt() method. The java.lang.Thread.interrupt() works if an object you are blocked on processes this signal correctly. For example, it works for InputStream.read() but doesn’t work for java.sql.PreparedStatement.execute(), so you have to test halt() method in each particular situation.
Once you are familiar with the Worker class, you can easily create your own implementation (Listing B). To run this class as a thread, simply use a new Thread(new WaitedWorker()).start. Applying Thread.interrupt() or Worker.halt() or a combination of them, you can control task execution precisely. For example, you can stop all workers when JVM shuts down by placing corresponding code in the java.lang.Runtime.addShutdownHook() method.
We've examined the long-running task framework and seen how to create new tasks based on its abstract class. Its architecture is clear and flexible and was designed with extensibility in mind. With this framework, you can avoid creating classes from scratch, and you'll be able to develop more efficient and reliable applications.