When multiple processes or threads access the same database simultaneously, race conditions can lead to data corruption, lost updates, and inconsistent results. This guide demonstrates common concurrency problems and their solutions.
Process B also increments to 101 and writes it—overwriting A’s update!
Lost Update Problem: When two processes run this code simultaneously, the final count may be significantly less than 2000 because updates overwrite each other.
The simplest and most efficient solution is to perform the increment entirely in SQL:
ContadorEnBDsoloUpdateSql.java
public class ContadorEnBDsoloUpdateSql { public static void main(String[] args) { final String claveContador = "contador1"; // The SQL UPDATE is atomic at the database level final String sqlActualización = "UPDATE contador SET cuenta=cuenta+1 WHERE nombre='" + claveContador + "';"; try { Class.forName("org.hsqldb.jdbc.JDBCDriver"); Connection connection = DriverManager.getConnection( "jdbc:hsqldb:file:./contadores_atomico;hsqldb.lock_file=false", "contadores", "987654321"); PreparedStatement actualización = connection.prepareStatement(sqlActualización); for (int i=0; i<1000; i++) { if (actualización.executeUpdate() != 1) break; } } catch (SQLException e) { System.out.println(e.getMessage()); } catch (Exception e) { e.printStackTrace(); } }}
Why This Works: The database guarantees that UPDATE contador SET cuenta=cuenta+1 is atomic. The read, increment, and write all happen as one indivisible operation.
SELECT ... FOR UPDATE - Lock row (Process B must wait)
Read value: 100
Increment: 101
UPDATE to 101
commit() - Release lock
Process B Waits
setAutoCommit(false) - Start transaction
SELECT ... FOR UPDATE - BLOCKS waiting for Process A’s lock
Lock acquired after A commits
Read value: 101 (A’s update)
Increment: 102
UPDATE to 102
commit() - Release lock
The key insight: FOR UPDATE creates an exclusive lock. Only one transaction at a time can hold the lock, forcing serial execution of the critical section.
The ContadorBuggy class shows proper transaction handling:
ContadorBuggy.java
public class ContadorBuggy { public static void main(String[] args) { String url = "jdbc:hsqldb:file:./contadores_atomico;hsqldb.lock_file=false"; try (Connection con = DriverManager.getConnection(url, "SA", "")) { con.setAutoCommit(false); inicializarBD(con); String sqlActualiza = "update contadores set cuenta = cuenta + 1 where nombre='contador1'"; try (PreparedStatement actualiza = con.prepareStatement(sqlActualiza)) { for (int i = 0; i < 1000; i++) { actualiza.executeUpdate(); } con.commit(); } catch (SQLException e) { con.rollback(); throw e; } // Verificar resultado try (Statement consulta = con.createStatement(); ResultSet res = consulta.executeQuery("select cuenta from contadores where nombre='contador1'")) { if (res.next()) { System.out.println("HSQLDB - Valor final: " + res.getInt(1)); } } } catch (SQLException e) { e.printStackTrace(); } }}
This wraps all 1000 updates in a single transaction. While it prevents partial failures, it doesn’t prevent concurrent access issues unless combined with proper locking.
// Add version column to track changesString sql = "UPDATE products SET price = ?, version = version + 1 " + "WHERE id = ? AND version = ?";pstmt.setDouble(1, newPrice);pstmt.setInt(2, productId);pstmt.setInt(3, currentVersion);int rowsUpdated = pstmt.executeUpdate();if (rowsUpdated == 0) { // Someone else updated it first - retry or fail throw new ConcurrentModificationException();}
To test race conditions, run multiple instances simultaneously:
# Terminal 1java ContadorEnBD &# Terminal 2java ContadorEnBD &# Wait for both to completewait# Check final countjava -cp . VerifyCounter
If the implementations are correct, running two processes simultaneously should produce a final count of 2000. With the buggy version, you’ll see counts less than 2000.