Hibernate uses the PreparedStatement#executeUpdate result to check the number of updated rows. If no row was matched, it then throws a StaleObjectStateException (when using Hibernate API) or an OptimisticLockException (when using JPA).
Optimistic locking is a generic-purpose concurrency control technique, and it works for both physical and application-level transactions.
So the stale exceptions prevent the "lost update" phenomena when multiple concurrent requests modify the same shared persistent data.
In an application-level transaction, once you load an entity you will get a logical repeatable read due to 1st level cache (Persistence Context), but other users can still modify the aforementioned entity.
So you can indeed run into stale entities, but the optimistic locking mechanism prevents loosing updates without taking any additional database locks, and it even works for long conversations.