The Counter Implementations demonstrate critical concurrency concepts in database programming. This tutorial compares six different approaches to incrementing a database counter, ranging from naive (buggy) implementations to production-ready transactional patterns.
All implementations target the same database table: contadores with columns nombre (VARCHAR) and cuenta (INT).
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 implementation appears correct but has a critical flaw:
Sets autoCommit(false) but batches all 1000 updates into one transaction
Updates ARE atomic within this single process
Problem: Multiple concurrent processes will have race conditions
Each process sees stale data because commits happen infrequently
Expected vs Actual Results
Single Process: Works perfectly (1000 increments)Multiple Processes: Race conditions occur:
Process A reads counter=0
Process B reads counter=0
Both increment to 1
Expected: 2, Actual: 1 (lost update)
ContadorBuggy.java:44-54
private static void inicializarBD(Connection con) throws SQLException { try (Statement stmt = con.createStatement()) { stmt.execute("CREATE TABLE IF NOT EXISTS contadores (nombre VARCHAR(50) PRIMARY KEY, cuenta INT)"); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM contadores WHERE nombre='contador1'"); if (rs.next() && rs.getInt(1) == 0) { stmt.execute("INSERT INTO contadores VALUES ('contador1', 0)"); } con.commit(); }}
This implementation is named “Buggy” because it fails under concurrent access despite using transactions. It’s a common mistake in database programming.
This approach uses JDBC’s updatable ResultSet feature:
ContadorEnBDUpdatable.java:10-46
public static void main(String[] args) { final String claveContador = "contador1"; final String sqlConsulta = "SELECT nombre,cuenta FROM contadores WHERE nombre=?"; try{ Class.forName("org.hsqldb.jdbc.JDBCDriver"); Connection connection = DriverManager.getConnection( "jdbc:hsqldb:file:./contadores_atomico;hsqldb.lock_file=false", "contadores ","987654321"); PreparedStatement consulta = connection.prepareStatement(sqlConsulta,ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE); consulta.setString(1, claveContador); int cuenta = 0; for (int i=0; i<1000;i++) { if (consulta.execute()) { ResultSet res = consulta.getResultSet(); if (res.next()) { cuenta = res.getInt(2)+1; res.updateInt(2, cuenta); res.updateRow(); } else System.out.println("Error"); } } System.out.println("Valor final: " + cuenta); } catch (SQLException e) { System.out.println(e.getMessage()); } catch (Exception e) { e.printStackTrace(); }}
Updatable ResultSet Configuration
PreparedStatement consulta = connection.prepareStatement( sqlConsulta, ResultSet.TYPE_SCROLL_INSENSITIVE, // Can scroll forward/backward ResultSet.CONCUR_UPDATABLE // Can update rows);
Key methods:
res.updateInt(columnIndex, newValue) - Update value in memory
res.updateRow() - Persist changes to database
Updatable ResultSets still suffer from race conditions because the read and update are not atomic. They’re primarily useful for single-user applications or when combined with transactions.
Production-ready implementation using FOR UPDATE row locking:
ContadorSqlTransaccional.java:9-43
public static void main(String[] args) throws ClassNotFoundException { // Prueba de concepto de transacción con bloqueo de fila para lectura // con el for update + transacción conseguimos el bloque de fila y atomicidad String sqlConsulta = "select nombre,cuenta from contadores where nombre='contador1' for update;"; String sqlActualización = "update contadores set cuenta=? where nombre='contador1';"; Class.forName("org.hsqldb.jdbc.JDBCDriver"); try (Connection connection = DriverManager.getConnection( "jdbc:hsqldb:file:./contadores_atomico;hsqldb.lock_file=false", "contadores", "987654321")) { PreparedStatement consulta = connection.prepareStatement(sqlConsulta); PreparedStatement actualización = connection.prepareStatement(sqlActualización); int cuenta = 0; for (int i=0; i<1000; i++) { connection.setAutoCommit(false); ResultSet res = consulta.executeQuery(); if (res.next()) { cuenta = res.getInt(2); cuenta++; actualización.setInt(1, cuenta); actualización.executeUpdate(); } else break; connection.commit(); connection.setAutoCommit(false); } // for System.out.println("Valor final: " + cuenta); } catch (Exception e) { e.printStackTrace(); }}
1. BEGIN TRANSACTION (setAutoCommit(false))2. SELECT ... FOR UPDATE (acquires lock)3. Read value4. Increment in Java5. UPDATE with new value6. COMMIT (releases lock)
Concurrency Guarantee
With FOR UPDATE, concurrent processes wait in queue:
Process A locks row at t1
Process B tries to read at t2, blocks and waits
Process A commits at t3, releases lock
Process B acquires lock at t4, reads latest value
Result: Perfect serialization, no race conditions!
Combines transactions, row locking, and updatable ResultSets:
ContadorSqlTransaccionalResUpdateable.java:9-44
public static void main(String[] args) throws ClassNotFoundException { // con el for update + transacción conseguimos el bloque de fila y atomicidad String sqlConsulta = "select nombre,cuenta from contadores where nombre='contador1' for update;"; Class.forName("org.hsqldb.jdbc.JDBCDriver"); try (Connection connection = DriverManager.getConnection( "jdbc:hsqldb:file:./contadores_atomico;hsqldb.lock_file=false", "contadores", "987654321")) { PreparedStatement consulta = connection.prepareStatement(sqlConsulta,ResultSet.FETCH_FORWARD, ResultSet.CONCUR_UPDATABLE); int cuenta = 0; for (int i=0; i<1000; i++) { connection.setAutoCommit(false); ResultSet res = consulta.executeQuery(); if (res.next()) { cuenta = res.getInt(2); cuenta++; // Exije que nombre sea clave primaria !!!! res.updateInt(2, cuenta); res.updateRow(); } else break; connection.commit(); connection.setAutoCommit(false); } // for System.out.println("Valor final: " + cuenta); } catch (Exception e) { e.printStackTrace(); }}
This implementation requires nombre to be a PRIMARY KEY for updateRow() to work correctly. The database needs a unique identifier to locate the row for updates.
Problem: Two processes read same value, both update, one overwrites the otherSolution: Use FOR UPDATE or SQL-level atomic operations
-- Wrong: Read-then-updateSELECT cuenta FROM contadores WHERE nombre='c1'; -- Process A reads 10-- (Process B reads 10 here)UPDATE contadores SET cuenta=11 WHERE nombre='c1'; -- A writes 11-- (Process B writes 11 here - lost A's update!)-- Right: Atomic updateUPDATE contadores SET cuenta=cuenta+1 WHERE nombre='c1'; -- Atomic
Deadlocks
Problem: Process A locks row 1 and waits for row 2; Process B locks row 2 and waits for row 1Solution: Always acquire locks in the same order across all processes
Problem: Holding locks too long blocks other processesSolution: Keep transactions short, acquire locks late
// Wrong: Lock held during user inputconnection.setAutoCommit(false);ResultSet rs = stmt.executeQuery("SELECT ... FOR UPDATE");System.out.println("Enter new value:"); // Lock held here!String input = scanner.nextLine();stmt.executeUpdate("UPDATE ...");connection.commit();// Right: Get input first, then lockSystem.out.println("Enter new value:");String input = scanner.nextLine();connection.setAutoCommit(false);ResultSet rs = stmt.executeQuery("SELECT ... FOR UPDATE"); // Lock acquired latestmt.executeUpdate("UPDATE ...");connection.commit(); // Lock released quickly
To test these implementations under concurrent load:
# Compilejavac -cp ".:hsqldb.jar" ContadorSqlTransaccional.java# Run 10 instances simultaneouslyfor i in {1..10}; do java -cp ".:hsqldb.jar" ContadorSqlTransaccional &donewait# Check final countjava -cp ".:hsqldb.jar" -c "SELECT cuenta FROM contadores WHERE nombre='contador1'"
Expected result with correct implementations: cuenta = 10000