Вы заметили состояние гонки в примере многопоточности в Java? Как с этим бороться?
Опубликовано: 2019-12-11 Некоторое время назад я написал статью о примере Producer Consumer и о том, как лучше обрабатывать операции чтения/записи в Java. Аналогично, в этом уроке мы обсудим Race Condition
и Thread locking
.
Если у вас есть какие-либо из следующих вопросов, то вы находитесь в правильном месте:
- Пример условия гонки Java
- пример мьютекса java
- многопоточность — что такое состояние гонки?
- Условия гонки и критические участки
- Что такое состояние гонки?
- Как справиться с состоянием гонки в Java на примере
Почему возникает состояние гонки в Java?
Состояние гонки в Java возникает, когда two or more threads
пытаются same time
изменить/обновить общие данные.
Давайте посмотрим на логику программы ниже:
Это очень простой банковский пример, в котором вы будете
deposit
иwithdraw
суммы100 times
. Вы вносите 100 долларов в общей сложности 100 раз = 100 x 100 = 10 000 долларов и снимаете всего 50 долларов 100 раз = 50 x 100 = 5 000 долларов. По окончании программы в вашем банке должно быть 5000 долларов.
Вот шаги:
- Создайте класс CrunchifyRaceCondition.java
- Создать класс CrunchifyTransaction.java
- Создайте класс CrunchifyBankAccount.java
- Мы запустим класс CrunchifyRaceCondition, и он запустит цикл ввода и вывода средств 100 раз.
- Мы запустим его
with Synchronized block
чтобы проверить результат. - Мы запустим его
without Synchronized block
чтобы проверить результат.
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 ; } } } |
Пожалуйста, проверьте строки 24 и 34 выше. Сохраните это ключевое слово Synchronized
и запустите свою программу. Вы должны увидеть правильный результат, как показано на изображении ниже.
Теперь удалите ключевое слово synchronized из строк 24 и 34 и запустите ту же программу.
Возможно, вам придется запустить эту программу несколько раз, чтобы увидеть проблему. В java нет гарантии, что вы все время будете видеть состояние гонки.
Если у вас есть приложение уровня предприятия, и вы говорите о миллионах транзакций в секунду, то состояние гонки может привести к катастрофе для вашей компании.
Теперь вопрос в том, как избежать состояния гонки в вашем Java-приложении?
- Если состояние гонки связано с обновлениями некоторых общих структур данных в памяти, вам необходимо синхронизировать доступ и обновления к структуре данных соответствующим образом.
- Если состояние гонки находится в обновлениях вашей базы данных, вам необходимо реструктурировать свой SQL, чтобы использовать транзакции на соответствующем уровне детализации.
- Неплохая идея провести нагрузочное тестирование перед запуском в производство. Большая нагрузка может вызвать редкое состояние гонки. Лучше исправить до того, как исправить потом.
- Убедитесь, что у вас нет глобальной переменной, в которую вы записываете.
- В Java каждый объект имеет один и только один монитор и связанный с ним мьютекс. Однако у одного монитора есть несколько дверей, каждая из которых обозначена ключевым словом
synchronized
. Когда поток проходит через ключевое словоsynchronized
, он эффективно запирает все двери. - Конечно, если поток не проходит через ключевое слово
synchronized
, он не запер дверь, и какой-то другой поток может свободно ворваться в любое время.