Hibernates Handling of Pesimisstic Locking

Good morning,
I am trying to understand if Hibernate may do any management when a pessimistic lock is used on an entity when its using the same “persistence context”.

Basically, does Hibernate do any “clever” management to avoid what would be an obvious locking exception?

Example:

  1. Obtain Lock : Thread 1 : em.find(User.class,"Alice",LockModeType.PESSIMISTIC_WRITE);
  2. Obtain Lock : Thread 2 : em.find(User.class,"Alice",LockModeType.PESSIMISTIC_WRITE);

Will Hibernate not call the actual locking for update on thread 2 untill the transaction has been comitted for thread/session 1?

I was a bit confused as the docs suggested it should throw a Locking exception:
JPA - LockModeType Docs

A lock with LockModeType.PESSIMISTIC_WRITE can be obtained on an entity instance to force serialization among transactions attempting to update the entity data. A lock with LockModeType.PESSIMISTIC_READ can be used to query data using repeatable-read semantics without the need to reread the data at the end of the transaction to obtain a lock, and without blocking other transactions reading the data. A lock with LockModeType.PESSIMISTIC_WRITE can be used when querying data and there is a high likelihood of deadlock or update failure among concurrent updating transactions.

The persistence implementation must support the use of locks of type LockModeType.PESSIMISTIC_READ and LockModeType.PESSIMISTIC_WRITE with non-versioned entities as well as with versioned entities.

When the lock cannot be obtained, and the database locking failure results in transaction-level rollback, the provider must throw the PessimisticLockException and ensure that the JTA transaction or EntityTransaction has been marked for rollback.

When the lock cannot be obtained, and the database locking failure results in only statement-level rollback, the provider must throw the LockTimeoutException (and must not mark the transaction for rollback).

But when I have written such tests in Spring Boot using @PersistenceContext EntityManager em and run multiple queries with a pooled thread executor (with some fake “work” during the transaction) and could not cause a PessimisticLockException to occur, it seemed that it “knew” it would happen and was managed.

I don’t know if I am misunderstanding the implementation but any clarity or links to help would be great!

Thank you.

EDIT: Adding test case:

@SpringBootTest
public class LockingTest {

    @Autowired
    private TransactionWrapperBean transactionWrapperBean;

    @Test
    @Sql(statements = "insert into users (id,enabled,user_type)  values ('1',1,'CUSTOMER');")
    public void expectLockingExceptions() {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executorService.submit(() -> {
                try {
                    transactionWrapperBean.doJob();
                } catch (InterruptedException e) {
                    System.out.println(e.getMessage());
                    throw new RuntimeException(e);
                }
            });
        }
        await().atMost(2, TimeUnit.MINUTES)
                .until(() -> invocationCount.get() == 100);
    }
}

@Component
public class TransactionWrapperBean {

    @PersistenceContext
    private EntityManager entityManager;

    public static AtomicInteger invocationCount = new AtomicInteger(0);

    @Transactional
    public void doJob() throws InterruptedException {
        System.out.println("Doing Job!");
        User user = entityManager.find(User.class, "1", LockModeType.PESSIMISTIC_WRITE);
        Assertions.assertNotNull(user);
        Thread.sleep(100);
        entityManager.persist(user);
        System.out.println(invocationCount.incrementAndGet());
    }
}

Test output:

Doing Job!
Doing Job!
Doing Job!
Doing Job!
1
Doing Job!
2
Doing Job!
3
Doing Job!
4
Doing Job!
5
Doing Job!
6
Doing Job!
7
Doing Job!
8

....

Doing Job!
2025-01-16T10:09:33.488Z DEBUG 20698 --- [xxxxx] [ool-1-thread-10] org.hibernate.SQL                        :
select u1_0.id,u1_0.user_type,u1_0.enabled,u1_0.password,u1_0.username from users u1_0 where u1_0.id=? for update
89
Doing Job!
2025-01-16T10:09:33.589Z DEBUG 20698 --- [xxxxx] [pool-1-thread-4] org.hibernate.SQL                        : 
select u1_0.id,u1_0.user_type,u1_0.enabled,u1_0.password,u1_0.username from users u1_0 where u1_0.id=? for update
90
Doing Job!
2025-01-16T10:09:33.691Z DEBUG 20698 --- [xxxxx] [pool-1-thread-9] org.hibernate.SQL                        : 
select u1_0.id,u1_0.user_type,u1_0.enabled,u1_0.password,u1_0.username from users u1_0 where u1_0.id=? for update
91
92
93
94
95
96
97
98
99
100

Will Hibernate not call the actual locking for update on thread 2 untill the transaction has been comitted for thread/session 1?

Hibernate ORM will execute the statement and the database will block. You can turn on SQL logging as well and then you will see that all statements are executed in parallel, but only one thread can continue, because the database blocks execution so that only one connection can acquire the lock.

A PessimisticLockException might occur when the database rolls back a transaction due to e.g. a lock serialization issue (usually only a problem with Repeatable Read or Serializable isolation levels) or deadlock scenario.
A lock timeout exception on the other hand is usually thrown by the database when lock acquisition takes longer than a specified timeout on the database. Not every database has an option to specify lock timeouts though, but sometimes you can “emulate” this by specifying a statement timeout or simple define a transaction timeout.

Ah that clears things up for me somewhat!

In the case of a “problem with Repeatable or Serializeable isolation levels” would an example be say a query with the default @Transactional is used with Repeatable read but doesn’t use any locking could update a row, so once the pessimistic locked session comes to commit, it will then throw the PessimisticLockException?

Thank you for the swift reply!

Potentially, yeah. If you want to be sure, write a test. It could also throw a TransactionException AFAIU because such serialization checks might also happen only at commit time.