Build your own ObjectPool in Java to boost app speed
Increase the speed of your applications while reducing memory requirements
Summary
Object pooling allows the sharing of instantiated objects. Since different processes don't need to reinstantiate certain objects, there is no load time. And since the objects are returned to the pool, there is a reduction in garbage collection. Read on to learn, via the author's own example object pool design, how to employ this approach to boost performance and minimize memory use. (1,400 words)
By Thomas E. Davis
hen I first implemented object pooling (specifically for database connections) into a realtime, multiuser application, average transaction times dropped from 250 milliseconds to 30 milliseconds. Users actually noticed and commented on the drastic increase in performance.
The idea of object pooling is similar to the operation of your local library. When you want to read a book, you know that it's cheaper to borrow a copy from the library rather than purchase your own copy. Likewise, it is cheaper (in relation to memory and speed) for a process to borrow an object rather than create its own copy. In other words, the books in the library represent objects and the library patrons represent the processes. When a process needs an object, it checks out a copy from an object pool rather than instantiate a new one. The process then returns the object to the pool when it is no longer needed.
There are, however, a few minor distinctions between object pooling and the library analogy that should be understood. If a library patron wants a particular book, but all the copies of that book are checked out, the patron must wait until a copy is returned. We don't ever want a process to have to wait for an object, so the object pool will instantiate new copies as necessary. This could lead to an exhoribitant amount of objects lying around in the pool, so it will also keep a tally on unused objects and clean them up periodically.
My object pool design is generic enough to handle storage, tracking, and expiration times, but instantiation, validation, and destruction of specific object types must be handled by subclassing.
Now that the basics out of the way, lets jump into the code. This is the skeletal object:
public abstract class ObjectPool
{
private long expirationTime;
private Hashtable locked, unlocked;
abstract Object create();
abstract boolean validate( Object o );
abstract void expire( Object o );
synchronized Object checkOut(){...}
synchronized void checkIn( Object o ){...}
}
Internal storage of the pooled objects will be handled with two Hashtable objects, one for locked objects and the other for unlocked. The objects themselves will be the keys of the hashtable and their last-usage time (in epoch milliseconds) will be the value. By storing the last time an object was used, the pool can expire it and free up memory after a specied duration of inactivity.
Ultimately, the object pool would allow the subclass to specify the initial size of the hashtables along with their growth rate and the expiration time, but I'm trying to keep it simple for the purposes of this article by hard-coding these values in the constructor.
ObjectPool()
{
expirationTime = 30000; // 30 seconds
locked = new Hashtable();
unlocked = new Hashtable();
}
The checkOut() method first checks to see if there are any objects in the unlocked hashtable. If so, it cycles through them and looks for a valid one. Validation depends on two things. First, the object pool checks to see that the object's last-usage time does not exceed the expiration time specified by the subclass. Second, the object pool calls the abstract validate() method, which does any class-specific checking or reinitialization that is needed to re-use the object. If the object fails validation, it is freed and the loop continues to the next object in the hashtable. When an object is found that passes validation, it is moved into the locked hashtable and returned to the process that requested it. If the unlocked hashtable is empty, or none of its objects pass validation, a new object is instantiated and returned.
synchronized Object checkOut()
{
long now = System.currentTimeMillis();
Object o;
if( unlocked.size() > 0 )
{
Enumeration e = unlocked.keys();
while( e.hasMoreElements() )
{
o = e.nextElement();
if( ( now - ( ( Long ) unlocked.get( o ) ).longValue() ) >
expirationTime )
{
// object has expired
unlocked.remove( o );
expire( o );
o = null;
}
else
{
if( validate( o ) )
{
unlocked.remove( o );
locked.put( o, new Long( now ) );
return( o );
}
else
{
// object failed validation
unlocked.remove( o );
expire( o );
o = null;
}
}
}
}
// no objects available, create a new one
o = create();
locked.put( o, new Long( now ) );
return( o );
}
That's the most complex method in the ObjectPool class, it's all downhill from here. The checkIn() method simply moves the passed-in object from the locked hashtable into the unlocked hashtable.
synchronized void checkIn( Object o )
{
locked.remove( o );
unlocked.put( o, new Long( System.currentTimeMillis() ) );
}
The three remaining methods are abstract and therefore must be implemented by the subclass. For the sake of this article, I am going to create a database connection pool called JDBCConnectionPool. Here's the skeleton:
public class JDBCConnectionPool extends ObjectPool
{
private String dsn, usr, pwd;
public JDBCConnectionPool(){...}
create(){...}
validate(){...}
expire(){...}
public Connection borrowConnection(){...}
public void returnConnection(){...}
}
The JDBCConnectionPool will require the application to specify the database driver, DSN, username, and password upon instantiation (via the constructor). If this is Greek to you, don't worry, JDBC is another topic. Just bear with me until we get back to the pooling.
public JDBCConnectionPool( String driver, String dsn, String usr, String pwd )
{
try
{
Class.forName( driver ).newInstance();
}
catch( Exception e )
{
e.printStackTrace();
}
this.dsn = dsn;
this.usr = usr;
this.pwd = pwd;
}
Now we can dive into implementation of the abstract methods. As you saw in the checkOut() method, ObjectPool will call create() from its subclass when it needs to instantiate a new object. For JDBCConnectionPool, all we have to do is create a new Connection object and pass it back. Again, for the sake of keeping this article simple, I am throwing caution to the wind and ignoring any exceptions and null-pointer conditions.
Object create()
{
try
{
return( DriverManager.getConnection( dsn, usr, pwd ) );
}
catch( SQLException e )
{
e.printStackTrace();
return( null );
}
}
Before the ObjectPool frees an expired (or invalid) object for garbage collection, it passes it to its subclassed expire() method for any necessary last-minute clean-up (very similar to the finalize() method called by the garbage collector). In the case of JDBCConnectionPool, all we need to do is close the connection.
void expire( Object o )
{
try
{
( ( Connection ) o ).close();
}
catch( SQLException e )
{
e.printStackTrace();
}
}
And finally, we need to implement the validate() method that ObjectPool calls to make sure an object is still valid for use. This is also the place that any re-initialization should take place. For JDBCConnectionPool, we just check to see that the connection is still open.
boolean validate( Object o )
{
try
{
return( ! ( ( Connection ) o ).isClosed() );
}
catch( SQLException e )
{
e.printStackTrace();
return( false );
}
}
That's it for internal functionality. JDBCConnectionPool will allow the application to borrow and return database connections via these incredibly simple and aptly named methods.
public Connection borrowConnection()
{
return( ( Connection ) super.checkOut() );
}
public void returnConnection( Connection c )
{
super.checkIn( c );
}
This design has a couple of flaws. Perhaps the biggest is the possibility of creating a large pool of objects that never gets released. For example, if a bunch of processes request an object from the pool simultaneously, the pool will create all the instances necessary. Then, if all the processes return the objects back to the pool, but checkOut() never gets called again, none of the objects get cleaned up. This is a rare occurrence for active applications, but some back-end processes that have "idle" time might produce this scenario. I solved this design problem with a "clean up" thread, but I'll save that discussion for a follow-up article. I will also cover the proper handling of errors and propagation of exceptions to make the pool more robust for mission-critical applications.
I welcome any comments, criticisms, and/or design improvements you may have to offer.