The following example shows how pessimistic locking should work in ObjectDB (and JPA):
public class F2992 {
private static final String DB = "objectdb:my.odb";
private static final EntityManagerFactory emf = Persistence.createEntityManagerFactory(DB);
public static void main(String[] args) throws Exception {
initDB();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(F2992::threadA);
Thread.sleep(200);
executor.submit(F2992::threadB);
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
emf.close();
}
private static void initDB() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
if (em.find(MyEntity.class, 1L) == null) {
em.persist(new MyEntity(1L, "initial"));
}
em.getTransaction().commit();
em.close();
}
private static void threadA() {
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
System.out.println("Thread A: trying to lock...");
MyEntity e = em.find(MyEntity.class, 1L, LockModeType.PESSIMISTIC_WRITE);
System.out.println("Thread A: got lock, updating...");
e.setName("by A");
Thread.sleep(1000);
em.getTransaction().commit();
System.out.println("Thread A: committed");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
em.close();
}
}
private static void threadB() {
EntityManager em = emf.createEntityManager();
boolean locked = false;
try {
while (!locked) {
try {
em.getTransaction().begin();
System.out.println("Thread B: trying to lock...");
MyEntity e = em.find(MyEntity.class, 1L, LockModeType.PESSIMISTIC_WRITE);
System.out.println("Thread B: got lock, updating...");
e.setName("by B");
em.getTransaction().commit();
System.out.println("Thread B: committed");
locked = true;
} catch (Exception ex) {
System.out.println("Thread B: failed to lock, retrying...");
em.getTransaction().rollback();
Thread.sleep(500);
}
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
em.close();
}
}
@Entity
static class MyEntity {
@Id
private long id;
private String name;
public MyEntity() {}
public MyEntity(long id, String name) {
this.id = id;
this.name = name;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
}
The output is:
Thread A: trying to lock...
Thread A: got lock, updating...
Thread B: trying to lock...
Thread B: failed to lock, retrying...
Thread B: trying to lock...
Thread B: failed to lock, retrying...
Thread A: committed
Thread B: trying to lock...
Thread B: got lock, updating...
Thread B: committed
As expected, thread B first fails while thread A holds the lock, then succeeds after thread A commits.
However, if you omit the call to rollback
(or put it in a comment) in threadB, you get an infinite loop:
Thread A: trying to lock...
Thread A: got lock, updating...
Thread B: trying to lock...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread A: committed
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
Thread B: failed to lock, retrying...
In this case, thread B enters an infinite loop of failed attempts. This does not mean ObjectDB fails to release the lock. The problem is that once a lock exception occurs, JPA marks the transaction as rollback-only. Unless you explicitly call rollback()
and begin a new transaction, retries will always fail, even after the lock is released. This is the expected behaviour in JPA.