Locking in Hibernate JPA

Locking is very useful for the applications based on concurrency. Thanks to it, we can avoid the collision being a results from simultaneous updates to the same data by two or more different users.

In this article we'll discover how the locking mechanism works with Hibernate's implementation of JPA specification. We'll start by explain the locking concept. At this occasion, we'll also try to show how to achieve a data lock in Hibernate. The next parts will treat about locking mechanisms. The first described case will be optimistic locking. The next part will present pessimistic locking while the last one, all others available locking mechanisms.

Hibernate's lock sample

We should start by explaining lock mechanism. A lock occurs when two or more "clients" try to read or manipulate the same data at the same time. This mechanism protects data against being corrupted because a lock is exclusive. It means that client who acquired a lock to one row, will be able to do his operation entirely and nobody will corrupt this manipulation. Take a look at below schema:
- user#1 locks a row and make some updates on it
- user#2 wants to update the same row as user#1 but he can't; in fact, the row is already locked
- user#2 waits for user#1 to unlock the row
- user#1 unlocks the row
- user#2 can now update the row

This schema is pretty simple. So, let's try to translate it into Hibernate's JPA code:

show lock() source code

When you execute this code, for example with Maven's command mvn -Dtest=LockingSample test, you should see:

show test result of lock() method

What does happen here ? The first transaction (transaction1) acquires a exclusive lock. The transaction isn't committed, so the row remains locked even when a second transaction begins and changes locked entity name. Consequently, the entity's name is never changed because transaction1's lock doesn't allow another transactions to modify locked entity.

Optimistic locking

In the previous code sample, we saw an implementation of pessimistic locking. Before explaining it more in details, we'll start by explaining another locking type, optimistic locking. It's based on entities versioning. Take a look at following schema:
- user#1 modifies the entity which version is 30
- user#2 modifies the same entity
- user#1 saves its modifications and changes the entity's version to 31
- user#2 wants to save its changes but he can't - he hasn't the recent version of modified entity. An OptimisticLockException is thrown to avoid data corruption.

So, if two concurrent transactions try to modify the same row, the first commit wins. The changes from the second (later) transaction aren't saved in database (they're only in "normal" optimistic locking, you'll see it more in details after code sample). The central part of optimistic locking is entity versioning. It's made by adding @Version annotation, as here:

@Entity
@Table(name="products")
public class Product {

  private int id;
  private String name;
  private long version;

  // others getters/setters ommitted

  @Version
  @Column(name="lock_version")
  public long getVersion() {
	  return version;
  }
  public void setVersion(long version) {
	  this.version = version;
  }
}

In the database layer, we must add a new column, here called lock_version. Version field can be one of following types: int, Integer, short, Short, long, Long, java.sql.Timestamp. Let's take a look at our test case for optimistic locking:

@Test
public void optimisticLock() {
  final EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  assertTrue("Entity manager factory can't be null", emFactory != null);
  boolean wasOle = false;
  EntityManager em1 = emFactory.createEntityManager();
  EntityManager em2 = emFactory.createEntityManager();
  String newName = ""+new Date().getTime();
  try {
    em1.getTransaction().begin();
    Product prod1 = em1.find(Product.class, 1);
    em1.lock(prod1,  LockModeType.OPTIMISTIC);
    prod1.setName(newName);
    em2.getTransaction().begin();
    Product prod2 = em2.find(Product.class, 1);
    prod2.setName("other name");
    em1.getTransaction().commit();
    em2.getTransaction().commit();
  } catch (Exception e) {
    if (e.getCause().getClass() == OptimisticLockException.class) {
      wasOle = true;
    }
  }
  assertTrue("OptimisticLockException should be catched but it wasn't", wasOle);
  
  Product product = em1.find(Product.class, 1);
  assertTrue("Product's name should be '"+newName+"' but was "+product.getName(), product.getName().equals(newName));
}

As you can see, we start by creating two concurrent EntityManager instances. Both will start their owns transactions and change the product name. At the end we see that the first transaction, working under optimistic locking, saves the changes directly before the second one. The second transaction which fails because of changes made by the first transaction. Hibernate considers that the prod2 object is out-of-date and launches OptimisticLockException with below stack trace:

show optimisticLock() source code

Because our last assertTrue case replies correctly, we can consider that the first transaction was committed into database and the second one wasn't. To be sure of that, you can make a simple SELECT query in MySQL:

mysql> select * from products;
+----+---------------+--------------+
| id | name          | lock_version |
+----+---------------+--------------+
|  1 | 1399740996188 |           75 |
|  2 | Product#2     |            0 |
+----+---------------+--------------+
2 rows in set (0.00 sec)

But they're another optimistic locking type, force increment mode. It's represented by OPTIMISTIC_FORCE_INCREMENT enum. It also throws javax.persistence.OptimisticLockException and commits the first transaction. Unlike "normal" optimistic locking, force increment mode will increment versioning field by 2 (2 which corresponds to the number of concurrent transactions). You can see it by replacing LockModeType.OPTIMISTIC by LockModeType.OPTIMISTIC_FORCE_INCREMENT and executing the test case. All should pass as previously, but in your database, you should see lock_version column incremented by 2:

mysql> select * from products;
+----+---------------+--------------+
| id | name          | lock_version |
+----+---------------+--------------+
|  1 | 1399741185805 |           77 |
|  2 | Product#2     |            0 |
+----+---------------+--------------+
2 rows in set (0.00 sec)

Pessimistic locking

Another locking mode is called pessimistic locking. Unlike optimistic, it doesn't need versionnng mechanism. Pessimistic locking is composed by 3 types:
- read: represented by LockModeType.PESSIMISTIC_READ, the entity is locked when it's read. The reads from another transactions are allowed but any another transaction can acquire pessimistic write lock.
- write: represented by LockModeType.PESSIMISTIC_WRITE, it locks entity on writing operation. Another transactions can't read or change this modified entity. It's translated by the impossibility to acquire pessimistic read and pessimistic write locks by other transactions.
- force increment: represented by LockModeType.PESSIMISTIC_FORCE_INCREMENT, this mode locks the entity at read time and increments version attribute if present. Another transactions can't acquire pessimistic read and write locks.

Pessimistic locking should be used in highly concurrent environments to ensure that the changes are made correctly (that they are no corrupted data). But it shouldn't be used on interactive web applications. It needs database resources as transaction and opened connection, to work properly. In consequence, it can leads into deadlocks. Suppose that one transaction acquires a lock for long operation. And after that another transactions try to get the lock on the same row, but they can't. They wait for the lock release but it takes too many time and the system freezes.

The main difference between optimistic and pessimistic locks is on theirs names. Optimistic lock hopes that your application won't need to handle any simultaneous updates. Pessimistic lock assumes the contrary. It thinks that your application will suffer on a lot of concurrent data operations. So it locks the data before making any changes.

Let's pass to code samples and start by trying to acquire pessimistic read when another transaction has already acquired pessimistic write on a row. Remember to comment or remove @Version annotation in tested entity class:

@Test
public void pessimisticWrite() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  assertTrue("Entity manager factory can't be null", emFactory != null);
  EntityManager entityManager1 = emFactory.createEntityManager();
  EntityTransaction transaction1 = entityManager1.getTransaction();
  try {
    transaction1.begin();
    Product prod1 = entityManager1.find(Product.class, 1);
    entityManager1.lock(prod1, LockModeType.PESSIMISTIC_WRITE);
    prod1.setName("apple");
  } catch (Exception e) {
    if (transaction1.isActive()) {
      transaction1.rollback();
    }
    LOGGER.error("An error occurred in transaction1", e);
  }
  
  boolean wasPle = false;
  EntityManager entityManager2 = emFactory.createEntityManager();
  EntityTransaction transaction2 = entityManager2.getTransaction();
  try {
    transaction2.begin();
    Product prod2 = entityManager2.find(Product.class, 1);
    entityManager2.lock(prod2, LockModeType.PESSIMISTIC_READ);
    prod2.setName("new name2"+new Date().getTime());
    transaction2.commit();
  } catch (Exception e) {
    if (transaction2.isActive()) {
      transaction2.rollback();
    }
    if (e.getCause().getClass() == PessimisticEntityLockException.class) {
      wasPle = true;
    }
    LOGGER.error("An error occurred in transaction2", e);
  }
  assertTrue("PessimisticEntityLockException didn't occured but it should", wasPle);
  
  transaction1.commit();
  Product apple = entityManager1.find(Product.class, 1);
  assertTrue("After commit, the product's name should be 'apple' but it wasn't", apple.getName().equals("apple"));
}

In this case, the first transaction acquires an write lock, called also exclusive. It means that another transactions can't lock the same entity as the first transaction. It causes an PessimisticEntityLockException and following stack trace:

show pessimisticWrite() test result

Now, let's test read lock, known also as shared lock because it can be used concurrently by two different transactions. But beware, it can be used only for reads. If two transactions write concurrently an entity, javax.persistence.LockTimeoutException is thrown. You can observe these two differences in following test cases:

@Test
public void pessimisticReadWithoutWrite() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  assertTrue("Entity manager factory can't be null", emFactory != null);
  EntityManager entityManager1 = emFactory.createEntityManager();
  EntityTransaction transaction1 = entityManager1.getTransaction();
  String name = "";
  try {
    transaction1.begin();
    Product prod1 = entityManager1.find(Product.class, 1);
    name = prod1.getName();
    entityManager1.lock(prod1, LockModeType.PESSIMISTIC_READ);
  } catch (Exception e) {
    fail("An exception occured but it shouldn't: "+e.getMessage());
    if (transaction1.isActive()) {
      transaction1.rollback();
    }
    LOGGER.error("An error occurred in transaction1", e);
  }
  
  EntityManager entityManager2 = emFactory.createEntityManager();
  EntityTransaction transaction2 = entityManager2.getTransaction();
  try {
    transaction2.begin();
    Product prod2 = entityManager2.find(Product.class, 1);
    if (!prod2.getName().equals(name)) {
	    fail("The name retreived by the second transaction should be the same as the name coming from the first one (expected:"+name+", was: "+prod2.getName()+")");
    }
    entityManager2.lock(prod2, LockModeType.PESSIMISTIC_READ);
    transaction2.commit();
  } catch (Exception e) {
    fail("An exception occured but it shouldn't: "+e.getMessage());
    if (transaction2.isActive()) {
      transaction2.rollback();
    }
    LOGGER.error("An error occurred in transaction2", e);
  }
  
  transaction1.commit();
  Product product = entityManager1.find(Product.class, 1);
  assertTrue("After commit, the product's name should be '"+name+"' but it wasn't", product.getName().equals(name));
}

@Test
public void pessimisticReadWithWrite() {
  EntityManagerFactory emFactory = Persistence.createEntityManagerFactory("sampleUnit");
  assertTrue("Entity manager factory can't be null", emFactory != null);
  EntityManager entityManager1 = emFactory.createEntityManager();
  EntityTransaction transaction1 = entityManager1.getTransaction();
  try {
    transaction1.begin();
    Product prod1 = entityManager1.find(Product.class, 1);
    entityManager1.lock(prod1, LockModeType.PESSIMISTIC_READ);
    prod1.setName("banana");
  } catch (Exception e) {
    if (transaction1.isActive()) {
      transaction1.rollback();
    }
    LOGGER.error("An error occurred in transaction1", e);
  }
  
  boolean wasLte = false;
  EntityManager entityManager2 = emFactory.createEntityManager();
  EntityTransaction transaction2 = entityManager2.getTransaction();
  try {
    transaction2.begin();
    Product prod2 = entityManager2.find(Product.class, 1);
    entityManager2.lock(prod2, LockModeType.PESSIMISTIC_READ);
    prod2.setName("carrot");
    transaction2.commit();
  } catch (Exception e) {
    if (transaction2.isActive()) {
      transaction2.rollback();
    }
    if (e.getCause().getClass() == LockTimeoutException.class) {
      wasLte = true;
    }
    LOGGER.error("An error occurred in transaction2", e);
  }
  assertTrue("PessimisticEntityLockException didn't occurred but it should", wasLte);
  
  transaction1.commit();
  Product product = entityManager1.find(Product.class, 1);
  assertTrue("After commit, the product's name should be 'banana' but it wasn't", product.getName().equals("banana"));
}

The first method, pessimisticReadWithoutWrite, makes only concurrent reads. So, no exception should occur and the entity retrieved at the end of the test should be the same as the entity read by the first and the second transaction. More interesting things take place in the second method, pessimisticReadWithWrite, where both transactions acquire pessimistic read lock and try to modify locked row. javax.persistance.LockTimeoutException is expected to be thrown because the lock reserved by the first transaction is released after the commit of the second one. The commit fails because of the first transaction's lock and its writing operation. A stack trace like this one could be retrieved in the logs:

show pessimistic methods test result

And finally, the changes from the first transaction are correctly saved in the database:

mysql> select * from products;
+----+--------+--------------+
| id | name   | lock_version |
+----+--------+--------------+
|  1 | banana |           81 |
|  2 | x      |            0 |
+----+--------+--------------+
2 rows in set (0.00 sec)

To see how does PESSIMISTIC_FORCE_INCREMENT work, let's take the sample for pessimistic write lock and replace lock modes for two entity managers by PESSIMISTIC_FORCE_INCREMENT. Comment also assertTrue("PessimisticEntityLockException didn't occured but it should", wasPle); and relaunch the test. Do not forget to uncomment @Version attribute in tested entity. The test should be executed correctly and product's name should be changed to 'apple'. Note that entity version is incremented by 2, exactly as for optimistic force increment lock:

mysql> select * from products;
+----+-------+--------------+
| id | name  | lock_version |
+----+-------+--------------+
|  1 | apple |           83 |
|  2 | x     |            0 |
+----+-------+--------------+
2 rows in set (0.01 sec)

This article introduced us to locking world in the Hibernate's JPA meaning. Two lock types are available. The first one, optimistic, should be implemented on environments with not many concurrent database operations. The second one, pessimistic, is destined to be use in highly concurrent applications. Both support versioning, translated by @Version annotation, incrementing versioning column (even when transaction should fail because of concurrency). Different types of exceptions, sometimes wrapped to javax.persistence.RollbackException, can occur (OptimisticLockException, LockTimeoutException or PessimisticEntityLockException).


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!