Locking
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Concurrent writers to the same row need a strategy for who wins. This page covers Hibernate’s optimistic and
pessimistic locking mechanisms in depth; for how Spring layers @Transactional isolation and @Lock on top of
these, see Transaction Isolation & Locking.
Optimistic locking with @Version
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
private BigDecimal balance;
}
Every UPDATE Hibernate generates for a @Version-annotated entity includes WHERE id = ? AND version = ?
and increments the version column. If zero rows match (because another transaction already updated — and
incremented the version of — that row first), Hibernate throws OptimisticLockException (JPA) /
StaleObjectStateException (native) instead of silently overwriting the concurrent change. This needs no
database-level locking at all — the conflict is only ever detected at write time, which is why it is called
optimistic: it assumes conflicts are rare and pays the cost only when one actually occurs.
OptimisticLockType/LockModeType variants:
| Mode | Behavior |
|---|---|
|
Standard |
|
Forces a version increment even when the entity’s own state did not change — used when a change to an
associated entity should still be treated as a conflict-relevant change to this entity (e.g. incrementing
an |
Versionless optimistic locking ( |
No |
@OptimisticLocking: dirty vs. all
@OptimisticLocking(type = OptimisticLockType.DIRTY) narrows the versionless comparison (or, combined with
@DynamicUpdate, the @Version-based one) to only the columns that actually changed in this flush, rather
than every mapped column — reduces the chance of a false conflict against a concurrent change to an unrelated
column, at the cost of needing @DynamicUpdate (see
Flushing & Dirty Checking) since the generated SQL now
varies per flush.
Pessimistic locking
Pessimistic locking asks the database to hold a row lock for the duration of the current transaction, blocking (or failing) other transactions that want a conflicting lock on the same row — suited to high-contention scenarios where an optimistic conflict would be common and retrying is expensive (e.g. decrementing a limited inventory count).
LockModeType |
Behavior |
|---|---|
|
A shared lock — blocks other transactions from acquiring a conflicting write lock, but not from also reading.
Maps to |
|
An exclusive lock — blocks any other transaction from reading-for-update or writing the row until this
transaction ends. Maps to |
|
Combines a pessimistic write lock with an immediate |
Account account = em.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE);
// or, explicitly against an already-loaded managed entity:
em.lock(account, LockModeType.PESSIMISTIC_WRITE);
Map<String, Object> hints = Map.of("jakarta.persistence.lock.timeout", 3000); // milliseconds
Account locked = em.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE, hints);
Hibernate’s native Session.lock(entity, LockMode) and Session.get(Class, id, LockOptions) offer the same
capability with finer-grained LockOptions (scope, timeout, and lockScope = LockOptions.Scope.EXTENDED to
also lock owned collections).
Lock scope and timeout hints
jakarta.persistence.lock.timeout (milliseconds; 0 = no wait/fail-fast, negative = wait indefinitely per the
database’s default) controls how long a pessimistic-lock acquisition waits before the database raises a lock — wait — timeout error, translated by Hibernate into jakarta.persistence.PessimisticLockException /
LockTimeoutException. jakarta.persistence.lock.scope = PessimisticLockScope.EXTENDED extends a pessimistic
lock on an entity to also lock rows in its owned collection tables, not just the entity’s own row.
Optimistic vs. pessimistic: choosing
Transaction
Isolation & Locking already lays out the general decision (contention frequency, retry cost, UX for
conflicts). At the Hibernate-mapping level specifically: optimistic locking needs no schema change beyond a
@Version column and scales better under low contention (no lock held across a possibly-slow transaction);
pessimistic locking avoids retries entirely under high contention but holds a real database lock for as long
as the transaction runs, directly limiting throughput on that row.