When your Java application has multiple transactions reading or modifying the same record in the database, data concurrency is a major issue that you need to address. A concurrency control mechanism, which applies locks to the records, enables data integrity and avoids any conflict of data. The two main types of locks used are pessimistic locking and optimistic locking.
In pessimistic locking, the object is locked when it is initially accessed for the first time in a given transaction. The lock then is released only when the transaction completes; the object is not accessible for any other transactions during the transaction.
In optimistic locking, the object is not locked when it is accessed for the first time in the transaction. Instead, its state (generally the version number) is saved. When other transactions that are accessing the same object try to modify the state of the object, the present state and the saved state are compared. If the states differ, then it's a clear indication of a conflicting update and the transaction will be rolled back.
Version 1.0 of the Java Persistence API (JPA) specification, which offered just the fundamental features of an object relational mapping (ORM) framework, supported only optimistic locking. JPA 2 added pessimistic locking support, bringing it more in line with the locking features in the Hibernate ORM framework. In this article, we explain pessimistic locking in JPA 2 and Hibernate and compare how the two technologies implement this feature.
Pessimistic Locking in JPA 2
JPA 2 supports pessimistic locking, with three new locking modes added to the existing optimistic locking such as like READ and WRITE. In all, these are all the locking modes available in JPA (the pessimistic locking modes are bold):
- READ (JPA1)
- WRITE (JPA1)
- OPTMISTIC (Synonymous to READ)
- OPTIMISTIC_FORCE_INCREMENT (Synonymous to WRITE)
- PESSIMISTIC_READ (JPA2)
- PESSIMISTIC_WRITE (JPA2)
- PESSIMISTIC_FORCE_INCREMENT (JPA2)
In this section, we explore the JPA 2 locking modes for pessimistic locking.
PESSIMISTIC_READ mode generally represents a shared lock. In this mode the EntityManager holds the lock on an entity during read operations as soon as the transaction begins. It is not released until the transaction is completed. This lock is best used when you access data that is not frequently modified, as it allows other transactions to read the entity.
Here is an example of using PESSIMISTIC_READ:
......// Transaction t1 beginst1.begin();// Employee entity is read from the databaseEmployee e = em.find(Employee.class, 1);// Lock is performed after readem.lock(e, LockModeType.PESSIMISTIC_READ);............// Transaction is committedt1.commit();...
At the time of writing, most of the persistence providers such as EclipseLink and Hibernate provided support for PESSIMISTIC_READ through PESSIMISTIC_WRITE.
The PESSIMISTIC_WRITE mode generally represents an exclusive lock. In this mode the EntityManager holds the lock on an entity as soon as the entity is updated in a transaction. This lock is best used in when there is a very high probability of update failure due to multiple transactions accessing the object.
Here is an example of using PESSIMISTIC_WRITE:
......// Transaction t1 beginst1.begin();// Employee entity is read and write lock is appliedEmployee e = em.find(Employee.class, 1, LockModeType.PESSIMISTIC_WRITE);............// Transaction is committedt1.commit();...
In this mode the EntityManager holds the lock on an entity when a transaction reads the entity. The version number is incremented towards the end of the transaction, irrespective of whether the entity was updated or not.
To understand the concept of PESSIMISTIC_FORCE_INCREMENT better, suppose two transactions (t1 and t2) occur concurrently. The t1 transaction locks the department instance by using PESSIMISTIC_FORCE_INCREMENT to ensure that no other transaction (say, t2) can update the state of the same department instance. In the first code snippet below, a lock is performed on the department entity and a flush method of the EntityManager is invoked so that the version number is incremented in the database even if the entity is not modified. After flush, the thread is made to sleep for a while. At the same time, another transaction (t2) tries to update the same department instance, which results in an exception and the t2 transaction getting rolled back.
// Transaction1 t1 // Transaction t1 beginst1.begin();// Get an instance of Employee entity with Id 1Emp e = em.find(Emp.class, 1);// Get the department of Employee with Id 1 which returns DEP 'A'Dep d = e.getDept();// Perform lock on department entity dem.lock(d, LockModeType.PESSIMISTIC_FORCE_INCREMENT);// Flush the entity manager, so that it increments the version numberem.flush();// Make the Thread to sleepThread.sleep(20000);// Transaction is committed after the thread resumes...t1.commit();// Transaction 2 t2// Transaction t2 beginst2.begin();// Get an instance of Department entity with Id 'A'Dep d = em.find(Dep.class, 'A');// Modify the state of the Department entityd.setDeptName("RESEARCH");// Transaction is committedt2.commit();
You can lock the entities in JPA 2 using the
find() methods of the EntityManager. When
EntityManager.refresh() is invoked it refreshes the state of the entity and applies a lock to it. You can also use the
setLockMode() method of the Query interface to set a lock mode to an entity. For
NamedQuery annotations, you can use the