Spring Boot 3 Concurrency: Locks, Optimistic & Pessimistic Updates

This article explains common problems of concurrent database updates in Spring Boot 3 and demonstrates five practical solutions—including database locks, optimistic and pessimistic locking, transaction isolation levels, and application‑level locks—accompanied by complete code examples and test results.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Boot 3 Concurrency: Locks, Optimistic & Pessimistic Updates

1. Introduction

Concurrent database updates occur when multiple users or threads try to modify the same record simultaneously, leading to data inconsistency, lost updates, dirty reads, and non‑repeatable reads.

2. Solutions

Database lock : use row/table locks with @Transactional to ensure a single‑transaction update.

Optimistic lock : add a @Version field to the entity and handle OptimisticLockingFailureException with @Retryable.

Pessimistic lock : apply SELECT ... FOR UPDATE via @Lock(LockModeType.PESSIMISTIC_WRITE) in the repository.

Transaction isolation : set isolation level to Isolation.SERIALIZABLE to serialize conflicting transactions.

Application lock : use Java synchronized, Lock, or distributed lock to guarantee exclusive execution of critical code.

3. Core Code Examples

Entity definition:

@Entity
@Table(name = "t_product")
public class Product {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  private BigDecimal price;
  private Integer quantity;
}

Repository interface:

public interface ProductRepository extends JpaRepository<Product, Long> {
  @Modifying
  @Query("update Product p set quantity = ?2 where id = ?1")
  int updateQuantity(Long id, Integer quantity);
}

Service method using a database lock:

@Transactional
public void productQuantity(Long id, Integer quantity) {
  // exclusive update within a transaction
  productRepository.updateQuantity(id, quantity);
  // simulate a long operation
  if (quantity == 20000) {
    System.err.printf("%s - entering wait...%n", Thread.currentThread().getName());
    try { TimeUnit.SECONDS.sleep(200); } catch (InterruptedException e) {}
    System.err.printf("%s - wait ended...%n", Thread.currentThread().getName());
  }
  System.out.printf("%s - update completed...%n", Thread.currentThread().getName());
}

Running two threads shows that the second thread blocks until the first transaction finishes (see image).

Optimistic lock example – add a version field:

public class Product {
  @Version
  private Integer version;
}

Service method with retry:

@Retryable(maxAttempts = 3, retryFor = OptimisticLockingFailureException.class)
public void productQuantity(Long id, Integer quantity) {
  productRepository.findById(id).ifPresent(product -> {
    product.setQuantity(quantity);
    // simulate delay
    if (quantity == 20000) {
      System.err.printf("%s - entering wait%n", Thread.currentThread().getName());
      try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) {}
    }
    try { productRepository.save(product); } catch (Exception e) { e.printStackTrace(); }
  });
}

When two threads execute the method, the second finishes quickly, while the first eventually throws an optimistic‑lock exception (see image).

Pessimistic lock implementation:

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);

@Transactional
public void productQuantity(Long id, Integer quantity) {
  productRepository.findById(id).ifPresent(product -> {
    product.setQuantity(quantity);
    if (quantity == 20000) {
      System.err.printf("%s - entering wait%n", Thread.currentThread().getName());
      try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) {}
    }
    productRepository.save(product);
  });
}

Running two threads shows the first thread holds the lock, the second waits (see images).

Transaction isolation level example:

@Transactional(isolation = Isolation.SERIALIZABLE)
public void productQuantity(Long id, Integer quantity) {
  System.err.printf("%s - preparing%n", Thread.currentThread().getName());
  productRepository.updateQuantity(id, quantity);
  if (quantity == 20000) {
    System.err.printf("%s - entering wait...%n", Thread.currentThread().getName());
    try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) {}
  }
  System.out.printf("%s - update completed...%n", Thread.currentThread().getName());
}

Two threads executed under SERIALIZABLE isolation also serialize their updates (see images).

Application‑level lock example:

private final Object lock = new Object();
public void productQuantity(Long id, Integer quantity) {
  synchronized (lock) {
    productRepository.updateQuantity(id, quantity);
  }
}

This ensures only one thread updates the quantity at a time (see image).

concurrencySpring BootOptimistic Lockpessimistic-lockTransaction Management
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.