¿Ha notado la condición de carrera en el ejemplo de concurrencia de subprocesos múltiples de Java? ¿Cómo lidiar con ello?
Publicado: 2019-12-11 Hace algún tiempo escribí un artículo sobre Producer Consumer Example y cómo manejar mejor la operación de lectura/escritura en Java. En la nota similar, en este tutorial discutiremos algo sobre Race Condition
y el Thread locking
.
Si tiene alguna de las siguientes preguntas, entonces está en el lugar correcto:
- Ejemplo de condición de carrera de Java
- ejemplo java mutex
- subprocesamiento múltiple: ¿qué es una condición de carrera?
- Condiciones de carrera y tramos críticos
- ¿Qué es la condición de carrera?
- Cómo lidiar con la condición de carrera en Java con un ejemplo
¿Por qué ocurre la condición de carrera en Java?
La condición de carrera en Java ocurre cuando two or more threads
intentan modificar/actualizar datos compartidos al same time
.
Echemos un vistazo a la lógica del programa a continuación:
Este es un ejemplo bancario muy simple en el que
deposit
ywithdraw
montos100 times
. Depositará $100 en total 100 veces = $100 x 100 = $10,000 y retirará $50 en total 100 veces = $50 x 100 = $5,000. Al final de la finalización del programa, debe tener $ 5000 en su banco.
Aquí están los pasos:
- Crear clase CrunchifyRaceCondition.java
- Crear clase CrunchifyTransaction.java
- Crear clase CrunchifyBankAccount.java
- Ejecutaremos la clase CrunchifyRaceCondition y comenzará el ciclo de depósito y retiro 100 veces.
- Lo ejecutaremos
with Synchronized block
para verificar el resultado. - Lo ejecutaremos
without Synchronized block
para verificar el resultado
CrunchifyRaceCondition.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package com . crunchify . tutorial ; /** * @author Crunchify.com * */ public class CrunchifyRaceCondition { public static void main ( String [ ] args ) { CrunchifyBankAccount crunchifyAccount = new CrunchifyBankAccount ( "CrunchifyAccountNumber" ) ; // Total Expected Deposit: 10000 (100 x 100) for ( int i = 0 ; i < 100 ; i ++ ) { CrunchifyTransaction t = new CrunchifyTransaction ( crunchifyAccount , CrunchifyTransaction . TransactionType . DEPOSIT_MONEY , 100 ) ; t . start ( ) ; } // Total Expected Withdrawal: 5000 (100 x 50) for ( int i = 0 ; i < 100 ; i ++ ) { CrunchifyTransaction t = new CrunchifyTransaction ( crunchifyAccount , CrunchifyTransaction . TransactionType . WITHDRAW_MONEY , 50 ) ; t . start ( ) ; } // Let's just wait for a second to make sure all thread execution completes. try { Thread . sleep ( 1000 ) ; } catch ( InterruptedException e ) { System . out . println ( e ) ; } // Expected account balance is 5000 System . out . println ( "Final Account Balance: " + crunchifyAccount . getAccountBalance ( ) ) ; } } |
CrunchifyTransaction.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
package com . crunchify . tutorial ; /** * @author Crunchify.com */ class CrunchifyTransaction extends Thread { public static enum TransactionType { DEPOSIT_MONEY ( 1 ) , WITHDRAW_MONEY ( 2 ) ; private TransactionType ( int value ) { } } ; private TransactionType transactionType ; private CrunchifyBankAccount crunchifyAccount ; private double crunchifyAmount ; /* * If transactionType == 1, depositAmount() else if transactionType == 2 withdrawAmount() */ public CrunchifyTransaction ( CrunchifyBankAccount crunchifyAccount , TransactionType transactionType , double crunchifyAmount ) { this . transactionType = transactionType ; this . crunchifyAccount = crunchifyAccount ; this . crunchifyAmount = crunchifyAmount ; } public void run ( ) { switch ( this . transactionType ) { case DEPOSIT_MONEY : depositAmount ( ) ; printBalance ( ) ; break ; case WITHDRAW_MONEY : withdrawAmount ( ) ; printBalance ( ) ; break ; default : System . out . println ( "NOT A VALID TRANSACTION" ) ; } } public void depositAmount ( ) { this . crunchifyAccount . depositAmount ( this . crunchifyAmount ) ; } public void withdrawAmount ( ) { this . crunchifyAccount . withdrawAmount ( crunchifyAmount ) ; } public void printBalance ( ) { System . out . println ( Thread . currentThread ( ) . getName ( ) + " : TransactionType: " + this . transactionType + ", Amount: " + this . crunchifyAmount ) ; System . out . println ( "New Account Balance: " + this . crunchifyAccount . getAccountBalance ( ) ) ; } } |

CrunchifyBankAccount.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package com . crunchify . tutorial ; /** * @author Crunchify.com */ class CrunchifyBankAccount { private String crunchifyAccountNumber ; private double crunchifyAccountBalance ; public String getAccountNumber ( ) { return crunchifyAccountNumber ; } public double getAccountBalance ( ) { return crunchifyAccountBalance ; } public CrunchifyBankAccount ( String crunchifyAccountNumber ) { this . crunchifyAccountNumber = crunchifyAccountNumber ; } // Make a note of this line -- synchronized keyword added public synchronized boolean depositAmount ( double amount ) { if ( amount < 0 ) { return false ; } else { crunchifyAccountBalance = crunchifyAccountBalance + amount ; return true ; } } // Make a note of this line -- synchronized keyword added public synchronized boolean withdrawAmount ( double amount ) { if ( amount > crunchifyAccountBalance ) { return false ; } else { crunchifyAccountBalance = crunchifyAccountBalance - amount ; return true ; } } } |
Consulte las líneas 24 y 34 anteriores. Mantenga esa palabra clave Synchronized
y ejecute su programa. Debería ver el resultado correcto como lo ve en la imagen de abajo.
Ahora elimine la palabra clave sincronizada de las líneas 24 y 34 y ejecute el mismo programa.
Es posible que deba ejecutar este programa varias veces para ver un problema. En Java no hay garantía de que verá la condición de carrera todo el tiempo.
Si tiene una aplicación de nivel empresarial y está hablando de millones de transacciones por segundo, la condición de carrera puede causar un desastre para su empresa.
Ahora la pregunta es cómo evitar la condición de carrera en su aplicación Java.
- Si la condición de carrera está en las actualizaciones de algunas estructuras de datos en memoria compartidas, debe sincronizar el acceso y las actualizaciones a la estructura de datos de manera adecuada.
- Si la condición de carrera está en las actualizaciones de su base de datos, debe reestructurar su SQL para usar transacciones en el nivel adecuado de granularidad.
- No es una mala idea hacer pruebas de carga antes de pasar a producción. Más carga puede causar condiciones de carrera raras. Es mejor arreglarlo antes que arreglarlo después.
- Asegúrese de no tener ninguna variable global en la que escriba.
- En Java, cada objeto tiene un único monitor y mutex asociado. Sin embargo, el monitor único tiene varias puertas, cada una indicada por la palabra clave
synchronized
. Cuando un subproceso pasa por encima de la palabra clavesynchronized
, efectivamente bloquea todas las puertas. - Por supuesto, si un subproceso no pasa a través de la palabra clave
synchronized
, no ha cerrado la puerta, y algún otro subproceso puede entrar libremente en cualquier momento.