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