Czy zauważyłeś Race Condition w przykładzie wielowątkowej współbieżności Java? Jak sobie z tym poradzić?
Opublikowany: 2019-12-11 Jakiś czas temu napisałem artykuł o Producer Consumer Example i jak lepiej obsługiwać operacje odczytu/zapisu w Javie. W podobnym tonie, w tym samouczku omówimy coś na temat Race Condition
i Thread locking
.
Jeśli masz jakiekolwiek z poniższych pytań, jesteś we właściwym miejscu:
- Przykład warunków wyścigu w Javie
- przykład mutex java
- wielowątkowość – co to jest wyścig?
- Warunki wyścigu i sekcje krytyczne
- Co to jest stan wyścigu?
- Jak radzić sobie z Race Condition w Javie z przykładem
Dlaczego występuje sytuacja wyścigowa w Javie?
Sytuacja wyścigu w Javie występuje, gdy two or more threads
same time
próbuje zmodyfikować/zaktualizować współdzielone dane.
Rzućmy okiem na poniższą logikę programu:
To bardzo prosty przykład bankowy, w którym będziesz
deposit
iwithdraw
kwoty100 times
. Wpłacisz 100 $ w sumie 100 razy = 100 $ x 100 = 10 000 $ i wypłacisz łącznie 50 $ 100 razy = 50 $ x 100 = 5000 $. Po zakończeniu programu powinieneś mieć w swoim banku 5000 $.
Oto kroki:
- Utwórz klasę CrunchifyRaceCondition.java
- Utwórz klasę CrunchifyTransaction.java
- Utwórz klasę CrunchifyBankAccount.java
- Uruchomimy klasę CrunchifyRaceCondition, która uruchomi pętlę wpłat i wypłat 100 razy.
- Uruchomimy go
with Synchronized block
aby sprawdzić wynik - Uruchomimy go
without Synchronized block
aby sprawdzić wynik
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 ; } } } |
Sprawdź wiersz 24 i 34 powyżej. Zachowaj to Synchronized
słowo kluczowe i uruchom swój program. Powinieneś zobaczyć poprawny wynik, jak widać na poniższym obrazku.
Teraz usuń zsynchronizowane słowo kluczowe z linii 24 i 34 i uruchom ten sam program.
Może być konieczne wielokrotne uruchomienie tego programu, aby zobaczyć problem. W javie nie ma gwarancji, że cały czas będziesz widzieć stan wyścigu.
Jeśli masz aplikację na poziomie korporacyjnym i mówisz o milionach transakcji na sekundę, wyścig może spowodować katastrofę dla Twojej firmy.
Teraz pytanie brzmi, jak uniknąć Race Condition w swojej aplikacji Java?
- Jeśli sytuacja wyścigu dotyczy aktualizacji niektórych współużytkowanych struktur danych w pamięci, należy odpowiednio zsynchronizować dostęp i aktualizacje struktury danych.
- Jeśli sytuacja wyścigu dotyczy aktualizacji bazy danych, musisz zmienić strukturę swojego kodu SQL, aby używać transakcji na odpowiednim poziomie szczegółowości.
- Nie jest złym pomysłem przeprowadzenie testów obciążenia przed uruchomieniem w środowisku produkcyjnym. Większe obciążenie może powodować rzadki stan wyścigu. Lepiej to naprawić, a dopiero później naprawić.
- Upewnij się, że nie masz zmiennej globalnej, do której piszesz.
- W Javie każdy obiekt ma jeden i tylko jeden powiązany z nim monitor i muteks. Pojedynczy monitor ma jednak kilka drzwi, z których każde wskazuje słowo kluczowe
synchronized
. Kiedy wątek przechodzi przezsynchronized
słowo kluczowe, skutecznie blokuje wszystkie drzwi. - Oczywiście, jeśli wątek nie przechodzi przez
synchronized
słowo kluczowe, nie zablokował drzwi, a jakiś inny wątek jest w każdej chwili wolny.