DekGenius.com
[ Team LiB ] Previous Section Next Section

10.3 Implementing Concurrency

Now we have a basic framework for locking and concurrency management; let's look at three patterns for implementing locking and concurrency in various environments.

10.3.1 Lockable Object Pattern

Some small applications keep their entire domain model in memory. This ability makes business logic easier to program, since you only need to deal with objects in memory (persistence, in this sort of application, can involve anything from serializing the entire object graph to disk to periodically writing changes to a database). The Lockable Object pattern is a simple approach to implementing locking in a nondistributed system where a single instance of a single application handles all changes to the data.

You can implement a simple lockable object using the Java synchronized keyword. As long as all attempts to access the object are synchronized properly, you don't have to worry about lost updates, dirty reads, or other concurrency problems. Unfortunately, synchronization has a few problems. First, each thread accessing a synchronized object blocks until the object becomes available, potentially tying up large numbers of threads while waiting for time-consuming processing to complete. Second, synchronized doesn't help if a user needs to hold onto an object across multiple threads: for instance, a web-based update process spread across two or three requests for a servlet.

To create a solution that lasts across threads, we need to be user-aware. For a lockable object, we accomplish this by creating a Lockable interface, which can be implemented by all of the data objects that might be subject to locking. One such an interface is shown in Example 10-4.

Example 10-4. Lockable interface for the Lockable Object pattern
public interface Lockable {
    public boolean isLocked(  );
    public void lock(String username) throws LockingException;
    public void unlock(String username) throws LockingException;
}

When an application wants to use an object that implements the Lockable interface, it calls the lock( ) method with the username of the current user. If the object throws a LockingException, then no lock was obtained and the system should either wait and try again later or deliver a complaint to the user. Otherwise, it has a lock and can make whatever updates it needs. The application is responsible for calling unlock( ) when it's finished with the object.

Example 10-5 shows a simple object implementing the locking interface. You can, of course, develop a more sophisticated interface. One obvious extension would be to add a timeout to each lock. Depending on your application's needs, you can either implement the Lockable interface separately for each object, or implement it in a base class from which each of your data objects descends.

Example 10-5. Customer object with locking interface
public class Customer implements Lockable {

    private String lockingUser = null;
    private Object lockSynchronizer = new Object(  );

    public void lock(String username) throws LockingException {
        if (username == null) throw new LockingException("No User Provided.");
        synchronized(lockSynchronizer) {
            if(lockingUser == null)
                lockingUser = username;
            else if ((lockingUser != null) && (!lockingUser.equals(username)))
                throw new LockingException("Resource already locked");
        }
    }

    public void unlock(String username) throws LockingException {
        if((lockingUser != null) && (lockingUser.equals(username)))
            lockingUser = null;
        else if (lockingUser != null)
            throw new LockingException("You do not hold the lock.");
    }

    public boolean isLocked(  ) {
        return (lockingUser != null);
    }

    // Customer getter/setter methods go here
}

One reviewer of this book rightly pointed out that the locking behavior in the example above belongs in a base class rather than in an interface implementation. The behavior of Example 10-5 is extremely generic, and there is, indeed, no reason why it shouldn't be provided in a base class. We chose to implement it as an interface for two reasons. First, locking logic might well change between different Lockable objects. Second, we might want to implement the same locking functionality on objects in a range of different object hierarchies, including some which already exist, or where we can't change the behavior of the base class. None of this changes the maxim that it always makes sense to implement functionality as high up the inheritance hierarchy as possible.

10.3.2 Lock Manager Pattern

Lockable objects themselves aren't always enough. Many DAO-based architectures don't maintain a particular object instance representing any particular bit of data within the system, especially when the application is spread across multiple servers. In these cases, we need a centralized registry of locks.

The Lock Manager pattern defines a central point for managing lock information by reference. Rather than assigning a lock based on a particular object instance, a lock manager controls locks based on an external object registry, which usually contains the primary keys associated with the objects under transaction control.

There are two common kinds of lock manager implementations: online and offline. Online lock management tracks everything within the JVM, and offline lock management uses an external resource, such as a database, to share locks across a number of applications. We'll look at both approaches next.

10.3.2.1 Online lock manager strategy

The simplest implementation approach for a lock manager is to handle everything in memory and in process. This is sometimes referred to as an online lock manager. Online lock managers are suitable for smaller applications contained in a single JVM.

Example 10-6 shows a lock manager implemented as a standalone Java object. The LockManager class provides methods for managing a set of managers—one for each type of resource you want to protect. Requesting a lock and releasing the lock are simple activities. This relatively simple implementation doesn't address some of the major production concerns: a more robust version would support releasing all locks for a user and setting an interval for the lock's expiration.

Even with synchronization code, this implementation is extremely fast: on a single-processor system with 10,000 active locks, the code can release and request several hundred thousand locks per second.

Example 10-6. LockManager.java
import java.util.*;

public class LockManager {
  private HashMap locks;

  private static HashMap managers = new HashMap(  );

  /**
   * Get a named Lock Manager. The manager will be created if not found.
   */
  public static synchronized LockManager getLockManager(String managerName) {
    LockManager manager = (LockManager)managers.get(managerName);
    if(manager == null) {
        manager = new LockManager(  );
        managers.put(managerName, manager);
    }
    return manager;
  }

  /**
   * Create a new LockManager instance.
   */
  public LockManager(  ) {
    locks = new HashMap(  );
  }

 /**
  * Request a lock from this LockManager instance.
  */
  public boolean requestLock(String username, Object lockable) {
    if(username == null)
      return false; // or raise exception
      
    synchronized(locks) {
      if(!locks.containsKey(lockable)) {
        locks.put(lockable, username);
        return true;
      }
      // Return true if this user already has a lock
      return (username.equals(locks.get(lockable)));
    }
  }

 /**
  * Release a Lockable object.
  */
  public Object releaseLock(Object lockable) {
    return locks.remove(lockable);
  }
}

To see how this works, consider the following code fragment:

CustomerBean obj1 = new CustomerBean(1);
CustomerBean obj2 = new CustomerBean(2);
CustomerBean obj3 = new CustomerBean(3);

LockManager lockManager = LockManager.getLockManager("CUSTOMER");

System.out.println("User 1, Obj1: " + lockManager.requestLock("user1", obj1));
System.out.println("User 2, Obj1: " + lockManager.requestLock("user2", obj1));
System.out.println("User 2, Obj2: " + lockManager.requestLock("user2", obj2));
System.out.println("User 1, Obj3: " + lockManager.requestLock("user1", obj3));
System.out.println("Release Obj1  " + lockManager.releaseLock(obj1));
System.out.println("User 2, Obj1: " + lockManager.requestLock("user2", obj1));

When run, this code produces the following output:

User 1, Obj1: true
User 2, Obj1: false
User 2, Obj2: true
User 1, Obj3: true
Release Obj1 user1
User 2, Obj1: true

When implementing this type of lock manager, it is important that you properly override the equals( ) and hashCode( ) methods on the objects you are locking. These methods are used by the HashMap object to keep track of different locks, and if you don't manage them properly, you may find yourself managing locks at a Java object level rather than at a data model object level. As a result, it often makes sense to handle locks based on a primary key object, rather than the actual object itself. In the example above, we could use java.lang.Long objects containing the customer's unique identifier, rather than the CustomerBean objects directly. It would eliminate uncertainty about where the CustomerBean came from, and make it easier to transition to an offline lock manager strategy as your application grows.

10.3.2.2 Offline lock manager strategy

An online lock manager is sufficient when the application runs on a single application server (whether that server is a simple servlet container or an expensive application server). It is insufficient, however, when application load is spread over multiple servers. Using an embedded lock manager prevents us from scaling the application horizontally by spreading users across multiple web servers accessing the same back end. Each server could give out a lock on the same resource with predictably disastrous consequences.

The solution is to shift lock management out of the servlet container and into the database. Doing this puts the locks closer to the shared resource, and allows us to put the same database behind as many web servers as we want without having to worry about two different servers giving different users simultaneous access to the same resources.

Example 10-7 provides a simple implementation of a database-backed lock manager. It uses a table named LOCK_TRACKING to keep track of locks. The LOCK_TRACKING table contains one row for each lock, and each row specifies the object type (allowing us to lock different kinds of objects with one lock manager), the object key (which we assume is a long), and the username of the user who did the locking. We also store the date the lock was obtained, and use it to create a method that releases all locks older than 15 minutes. Here's how the LOCK_TRACKING table is defined in Oracle:

create table lock_tracking (
  object_type   varchar2(30),
  object_key    number,
  username      varchar2(30) not null,
  obtained      date default sysdate,
  primary key (object_type, object_key);
);

We need to prevent multiple users from obtaining locks on the same type/key pair. We count on the fact that the JDBC driver will throw an SQLException if an insert operation fails. The primary key prevents the database from storing more than one row in the table for each type/key pair. We obtain a lock by inserting a row in the table, and delete the row to remove the lock, clearing the way for another user to obtain a lock.

Example 10-7. OfflineLockManager.java
import locking.LockingException;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class OfflineLockManager {
 private DataSource dataSource;
 private static final String LOCK_INSERT_STMT =
    "INSERT INTO LOCK_TRACKING "+
    "(OBJECT_TYPE, OBJECT_KEY, USERNAME) VALUES (?, ?, ?)";

 private static final String LOCK_SELECT_STMT =
    "SELECT OBJECT_TYPE, OBJECT_KEY, USERNAME, " + 
    "OBTAINED FROM LOCK_TRACKING WHERE " + 
    "OBJECT_TYPE = ? AND OBJECT_KEY = ?";

 private static final String RELEASE_LOCK_STMT =
    "DELETE FROM LOCK_TRACKING WHERE OBJECT_TYPE = ? "+
    "AND OBJECT_KEY = ? AND USERNAME = ?";

 private static final String RELEASE_USER_LOCKS_STMT =
    "DELETE FROM LOCK_TRACKING WHERE USERNAME = ?";

 // Oracle specific lock release statement; 
 // release all locks over 15 minutes (1/96 of a day)
 private static final String RELEASE_AGED_LOCKS_STMT =
    "DELETE FROM LOCK_TRACKING WHERE OBTAINED < SYSDATE - (1/96)";

 public OfflineLockManager(DataSource ds) {
   dataSource = ds;
 }

 public boolean getLock(String objectType, long key, String username)
         throws LockingException {

     Connection con = null;
     PreparedStatement pstmt = null;
     boolean gotLock = false;

     try {
       con = dataSource.getConnection(  );
       // use strict isolation
       con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
       con.setAutoCommit(false);
       pstmt = con.prepareStatement(LOCK_INSERT_STMT);
       pstmt.setString(1, objectType);
       pstmt.setLong(2, key);
       pstmt.setString(3, username);

       try {
          pstmt.executeUpdate(  );
          gotLock = true;
       } catch (SQLException ex) {
       } // a SQLException means a PK violation, which means an existing lock

       if (!gotLock) { 
          // This means there was a Primary Key violation: somebody has a lock!
          String lockingUsername = getLockingUser(con, objectType, key);
          if ((lockingUsername != null) && (lockingUsername.equals(username)))
             gotLock = true; // We already have a lock!
       }

       con.commit(  ); // end the transaction
     } catch (SQLException e) {
       try {
         con.rollback(  );
       } catch (SQLException ignored) {}
       LockingException le = new LockingException(e.getMessage(  ));
       le.initCause(e); // JDK 1.4; comment out for earlier JDK releases
       throw le;
     } finally {
        if (pstmt != null) 
            try { pstmt.close(  ); } catch (SQLException ignored) {}
        if (con != null) 
            try { con.close(  ); } catch (SQLException ignored) {}
     }

     return gotLock;
 }

 /**
 * Release a lock held by a given user on a particular type/key pair.
 */
 public boolean releaseLock(String objectType, long key, String username)
         throws LockingException {
   Connection con = null;
   PreparedStatement pstmt = null;
   try {
     con = dataSource.getConnection(  );
     pstmt = con.prepareStatement(RELEASE_LOCK_STMT);
     pstmt.setString(1, objectType);
     pstmt.setLong(2, key);
     pstmt.setString(3, username);
     int count = pstmt.executeUpdate(  );
     return (count > 0); // if we deleted anything, we released a lock.
   } catch (SQLException e) {
     LockingException le = new LockingException(e.getMessage(  ));
     le.initCause(e); // JDK 1.4; comment out for earlier JDK releases
     throw le;
   } finally {
     if (pstmt != null) 
         try { pstmt.close(  ); } catch (SQLException ignored) {}
     if (con != null) 
         try { con.close(  ); } catch (SQLException ignored) {}
   }
 }

 /**
  * Release all locks held by a particular user. 
  * Returns true if locks were release.
  */
 public boolean releaseUserLocks(String username) throws LockingException {
   Connection con = null;
   PreparedStatement pstmt = null;
   try {
     con = dataSource.getConnection(  );
     pstmt = con.prepareStatement(RELEASE_USER_LOCKS_STMT);
     pstmt.setString(1, username);
     int count = pstmt.executeUpdate(  );
     return (count > 0); // if we deleted anything, we released locks.
   } catch (SQLException e) {
     LockingException le = new LockingException(e.getMessage(  ));
     le.initCause(e); // JDK 1.4; comment out for earlier JDK releases
     throw le;
   } finally {
     if (pstmt != null) 
         try { pstmt.close(  ); } catch (SQLException ignored) {}
     if (con != null) 
         try { con.close(  ); } catch (SQLException ignored) {}
   }
 }

 /**
   * Release all locks over 15 minutes old.
   */
 public boolean releaseAgedLocks(  ) throws LockingException {
   Connection con = null;
   PreparedStatement pstmt = null;

   try {
      con = dataSource.getConnection(  );
      pstmt = con.prepareStatement(RELEASE_AGED_LOCKS_STMT);
      int count = pstmt.executeUpdate(  );
      return (count > 0); // if we deleted anything, we released locks.
   } catch (SQLException e) {
      LockingException le = new LockingException(e.getMessage(  ));
      le.initCause(e); // JDK 1.4; comment out for earlier JDK releases
      throw le;
   } finally {
      if (pstmt != null) 
          try { pstmt.close(  ); } catch (SQLException ignored) {}
      if (con != null) 
          try {  con.close(  ); } catch (SQLException ignored) {}
   }
 }


  /**
    * Returns the user currently hold a lock on this type/key pair, 
    * or null if there is no lock.
    */
  private String getLockingUser(Connection con, String objectType, 
                                long key) throws SQLException {
    PreparedStatement pstmt = null;
    try {
      pstmt = con.prepareStatement(LOCK_SELECT_STMT);
      pstmt.setString(1, objectType);
      pstmt.setLong(2, key);
      ResultSet rs = pstmt.executeQuery(  );
      String lockingUser = null;
      if (rs.next(  ))
        lockingUser = rs.getString("USERNAME");
        rs.close(  );
        return lockingUser;
    } catch (SQLException e) {
      throw e;
    } finally {
      if (pstmt != null) 
          try { pstmt.close(  ); } catch (SQLException ignored) {}
    }
 }

}

All of the locking examples in this chapter require the application to specify the user requesting the lock. This method works fairly well for simple applications, but it also creates a few problems. In particular, every piece of code needs to know the identifier associated with the current user or process. Passing this data around, particularly through multiple tiers and multiple levels of abstraction, can be pretty dicey—at best, it's one more parameter to include in every method call.

We can work around this problem somewhat by incorporating a username directly into the transaction context, much as we incorporated the database connection into the context. As a result, the current username is available for any code that needs it, without the intervening layers necessarily being aware of it. The JAAS security API uses a similar approach and can be productively integrated into a transaction and concurrency control scheme.

10.3.3 Version Number Pattern

Particularly in an optimistic concurrency scenario, resolving concurrency conflicts requires determining whether an object has changed—but we don't want to task the entire application with keeping track of changes. The Version Number pattern allows us to associate a simple record of state change with each data object by recording a version number that is incremented with each change to the underlying data. The version number can be used by DAOs and other objects to check for external changes and to report concurrency issues to the users. The version number can be persistent or transient, depending on the needs of your application. If transactions persist across crashes, server restarts, or acts of God, then we should include the version number as a field within the object, persisted along with the rest of the fields. We will implement a persistent version number if we're instantiating multiple copies of the object from persistent storage (for example, via a DAO).

If we create one object instance that remains in memory for the life of the system, we may not need a persistent version number.

Example 10-8 demonstrates a simple versioned object, with version information retrieved via the getVersion( ) method. We also provide an equality method, equalsVersion( ), that does a content-based comparison, including the version number. Depending on the needs of your application, you might want to make the equals( ) method version-aware as well. The equalsVersion( ) method allows you to determine whether an object has been touched as well as whether it has been changed. This can be important when your persistence layer needs to record a last update date: if the object is fed its original values, the version will be incremented while the data remain unchanged. This might indicate a meaningful update, if only to say that all's well.

Example 10-8. VersionedSample.java
public class VersionedSample implements Versioned {
    private String name;
    private String address;
    private String city;
    private long pkey = -1;
    
    private long version = 1;
   
    public VersionedSample(long primaryKey) {
        pkey = primaryKey;
    }
 
    public long getVersion(  ) {
        return version;
    }

    public String getName(  ) {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        version++;
    }

    public String getAddress(  ) {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
        version++;
    }

    public String getCity(  ) {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
        version++;
    }
    
    public boolean equalsVersion(Object o) {
        if (this == o) return true;
        if (!(o instanceof VersionedSample)) return false;

        final VersionedSample versionedSample = (VersionedSample) o;
        if(o.pkey != this.pkey) return false;

        if (version != versionedSample.version) return false;
        if (address != null ? !address.equals(versionedSample.address) :   
            versionedSample.address != null) return false;
        if (city != null ? !city.equals(versionedSample.city) : 
            versionedSample.city != null) return false;
        if (name != null ? !name.equals(versionedSample.name) :
            versionedSample.name != null) return false;

        return true;
    }
}
    [ Team LiB ] Previous Section Next Section