Что такое состояние гонки в java
Перейти к содержимому

Что такое состояние гонки в java

  • автор:

Состояние гонки

Состоя́ние го́нки (англ. race condition ) — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем (см. Гонки сигналов (электроника)).

Состояние гонки — специфическая ошибка, проявляющаяся в случайные моменты времени и «затихающая» при попытке её локализовать.

Пример

Рассмотрим пример кода (на Java) [1]

int x; 
// Поток 1: while (!stop)  x++;> 
// Поток 2: while (!stop)  if (x%2 == 0) System.out.println("x sy0">+ x);> 

Пусть x=0. Предположим, что выполнение программы происходит в таком порядке:

  1. if в потоке 2 проверяет x на чётность.
  2. Оператор «x++» в потоке 1 увеличивает x на единицу.
  3. Оператор вывода в потоке 2 выводит «x=1», хотя, казалось бы, переменная проверена на чётность.

Способы решения

Локальная копия

Самый простой способ решения — копирование переменной x в локальную переменную. Вот исправленный код:

// Поток 2: while (!stop)  int cached_x = x; if (cached_x%2 == 0) System.out.println("x sy0">+ cached_x);> 

Естественно, этот способ работает только тогда, когда переменная одна и копирование производится за одну машинную команду.

Синхронизация

Более сложный, но и более универсальный метод решения — синхронизация потоков, а именно: [1]

int x; 
// Поток 1: while (!stop)  synchronized(SomeObject)  x++; >> 
// Поток 2: while (!stop)  synchronized(SomeObject)  if (x%2 == 0) System.out.println("x sy0">+ x); >> 

Комбинированный способ

Предположим, что переменная x имеет тип не int , а long (на 32-битных ЭВМ её копирование выполняется за две машинных команды), а во втором потоке вместо System.out.println стоит более сложная обработка. В этом случае оба метода неудовлетворительны: первый — потому что x может измениться, пока идет копирование; второй — потому что засинхронизирован слишком большой объём кода.

Эти способы можно скомбинировать, копируя «опасные» переменные в синхронизированном блоке. С одной стороны, это снимет ограничение на одну машинную команду, с другой — позволит избавиться от слишком больших синхроблоков.

long x; 
// Поток 1: while (!stop)  synchronized(SomeObject)  x++; >> 
// Поток 2: while (!stop)  long cached_x; synchronized (SomeObject)  cached_x = x; > if (cached_x%2 == 0) //System.out.println("x br0">(cached_x);> 

Очевидных способов выявления и исправления состояний гонки не существует. Лучший способ избавиться от гонок — правильное проектирование многозадачной системы.

Случай с Therac-25

Основная статья: Therac-25

Аппарат лучевой терапии Therac-25 был первым в США медицинским аппаратом, в котором вопросы безопасности были возложены исключительно на программное обеспечение. Этот аппарат работал в трёх режимах:

  1. Электронная терапия: электронная пушка напрямую облучает пациента; компьютер задаёт энергию электронов от 5 до 25 МэВ.
  2. Рентгеновская терапия: электронная пушка облучает вольфрамовую мишень, и пациент облучается рентгеновскими лучами, проходящими через конусообразный рассеиватель. В этом режиме энергия электронов одна: 25 МэВ.
  3. В третьем режиме никакого излучения не было. На пути электронов (на случай аварии) располагается стальной отражатель, а излучение имитируется светом. Этот режим применяется для того, чтобы точно навести пучок на больное место.

Эти три режима задавались вращающимся диском, в котором было отверстие с отклоняющими магнитами для электронной терапии, и мишень с рассеивателем для рентгеновской. Из-за состояния гонки между управляющей программой и обработчиком клавиатуры иногда случалось, что в режиме рентгеновской терапии диск оказывался в положении «Электронная терапия», и пациент напрямую облучался пучком электронов в 25 МэВ, что вело к переоблучению. При этом датчики выводили «Нулевая доза», поэтому оператор мог повторить процедуру, усугубляя ситуацию. В результате погибли как минимум четыре пациента.

Часть кода была взята из Therac-6 и Therac-20. При этом в Therac-6 не было рентгеновской терапии, а в Therac-20 были аппаратные меры безопасности, которые не давали включить излучение, когда диск был в неправильном положении.

Взломы путём эксплуатирования состояния гонки

Существует класс ошибок (и эксплуатирующих их типов атак), позволяющих непривилегированной программе влиять на работу других программ через возможность изменения общедоступных ресурсов (обычно — вре́менных файлов; англ. /tmp race — состояние гонки во вре́менном каталоге), в определённое временно́е окно, в которое файл по ошибке программиста доступен для записи всем или части пользователей системы.

Атакующая программа может разрушить содержимое файла, вызвав аварийное завершение программы-жертвы, или, подменив данные, заставить программу выполнить какое-либо действие на уровне своих привилегий.

Именно по этой причине ПО с серьёзными требованиями по безопасности, такое, как веб-браузер, использует случайные числа криптографического качества для именования временных файлов.

Примечания

  1. 12 Ключевое слово volatile намеренно не поставлено — доступ к переменной с этим модификатором в Java синхронизируется автоматически.

См. также

  • Therac-25
  • Семафор (информатика)
  • Мютекс
  • Взаимная блокировка
  • Проблема ABA

Ссылки

Введение в многопоточность в Java очень простым языком: Процессы, Потоки и Основы синхронизации

На старте вашей карьеры вы вполне можете обойтись без практических навыков в параллельном программировании, но рано или поздно перед вами встанет задача, требующая от вас таких навыков.

Итак, в данной статье мы поговорим о многопоточности в Java. Тема очень обширная, и я не ставлю целью описать все ее аспекты. Статья рассчитана на людей, только начинающих свое знакомство с многопоточностью. В данной статье мы рассмотрим основу многопоточности Java, такие базовые механизмы синхронизации как ключевые слова volatile и synchronized и очень важную проблематику “Состояние гонки” и “Взаимная блокировка”.

Я выбрал немного необычный подход, связав технические примеры с нашей повседневной жизнью, надеюсь вам понравится. Тема будет раскрыта на примере абстрактной комнаты и людей в находящихся в ней.

Дабы максимально упростить материал, я намеренно буду опускать некоторые нюансы реализации и иерархии многопоточности в Java, усложняющие понимание темы. Если вы рассчитываете на подробный обзор с техническими терминами и формулировками, то данная статья вам не подойдет.

Что такое процессы и потоки

Прежде чем перейти к многопоточности, давайте разберемся, что такое процессы и потоки.

Процесс — это экземпляр выполняющейся программы, простыми словами при запуске любой программы на вашем компьютере, вы порождаете процесс. Он имеет свое собственное адресное пространство памяти и один или несколько потоков.

Поток — это последовательность инструкций, выполняющаяся внутри процесса. Потоки делят адресное пространство памяти процесса, что позволяет им работать параллельно.

Создание и управление потоками

Каждый раз когда вы запускаете вашу программу, т.е. порождаете процесс, JVM (виртуальная машина) создает для вас так называемы главный поток (main thread) в котором ваш код будет исполняться.

Из своего главного потока вы можете создавать множество других потоков которые будут исполняться параллельно вашему главному потоку.

В Java создание и управление потоками осуществляется с использованием класса Thread. Чтобы создать поток, необходимо унаследоваться от класса Thread и переопределить его метод run(), в котором указывается код, который будет выполняться в потоке. Затем создается экземпляр класса Thread и вызывается метод start(), чтобы запустить поток.

class MyThread extends Thread < public void run() < System.out.println("Этот код выполняется в потоке"); >> public class Main < public static void main(String[] args) < MyThread thread = new MyThread(); thread.start(); System.out.println("Этот код выполняется в главном потоке"); >>

Чтобы объяснить более простым языком что такое потоки и как они взаимодействуют, давайте абстрагируемся от кода и представим что ваша программа (процесс) — это комната, а потоки — это люди, находящиеся в этой комнате. В комнате есть различные предметы (объекты), с которыми люди могут взаимодействовать. Когда вы пускаете несколько людей в комнату (создаете новые потоки), они получают доступ к тем же самым предметам.

Однако, когда несколько людей одновременно пытаются взаимодействовать с одним и тем же предметом, могут возникать конфликты, такие как “Состояние гонки” (Race condition) и “Взаимная блокировка” (Deadlocks).

Состояние гонки

Состояние гонки (Race condition) — это ситуация, когда два или более потока одновременно обращаются к общим данным или ресурсам, и результаты их операций зависят от того, в каком порядке выполняются операции. Это может привести к непредсказуемым и нежелательным результатам, таким как неправильные значения или ошибки в программе. В результате состояния гонки данные или ресурсы могут быть повреждены или использованы неправильно.

Давайте рассмотрим “состояние гонки” в рамках нашего абстрактного примера. Представьте что в комнате стоит стол, а на нем полный стакан воды. В этой же комнате находятся два человека, скажем Саша и Петя. И вот Саша решил сделать глоток воды из этого стакана, чуть позже еще глоток, а потом и еще. В реальной комнате это выглядело бы примерно так: Саша каждый раз подходил бы к этому стакану, брал бы его, делал глоток и клал бы на место, а потом и сам возвращался на место. Но компьютер это не комната, в нем есть всякие механизмы оптимизации. К примеру, вместо того, чтобы заставлять Сашу ходить туда-сюда каждый раз когда он захотел сделать глоток воды, он создаст копию стакана для Саши когда тот придет за стаканом в первый раз.

Итак, Саша уже имеет свой стакан (копию того стакана, что остался стоять на столе), и ему уже не надо каждый раз ходить за стаканом, он просто сидит на своем месте и делает три глотка воды. Но, несмотря на то что у Саши копия стакана, а оригинал остался стоять на столе, он знает что должен его отнести на место и заменить своей копией оригинал, это процесс называется синхронизация и необходимо для поддержания актуального состояния уровня воды в стакане (значения переменной).

В то время как Саша пил из своей копии стакана, Петя тоже захотел глотнуть воды для чего подошел к столу где стоит все еще полный стакан воды потому как Петя еще не вернул свой стакан (синхронизация еще не произошла) и соответственно получил свою копию полного стакана, после чего вернулся на место и сделал два глотка. В то время как Петя пил из своей копии стакана, Саша уже закончил пить и отнес свой стакан на место и соответственно заменил им оригинальный стакан (совершил синхронизацию), в итоге количество воды в стакане на столе уменьшилось на 3 глотка. Через какое-то время Петя тоже закончил пить и отнес свой стакан на место и соответственно заменил им стакан, стоящий на столе. В итоге объем воды в стакане на столе — это полный стакан минус два глотка Пети, а глотки Саши утеряны. Это и есть состояние гонки и результаты ее могут быть абсолютно не предсказуемыми.

Для борьбы с этим явлением Java предоставляет различные механизмы, базовым является использование ключевого слова volatile.

Ключевое слово volatile используется для обозначения переменных, которые могут быть изменены несколькими потоками. Оно гарантирует, что изменения переменной видны другим потокам.

Проще говоря, если вернуться к примеру с Сашей и Петей, то каждый раз, когда кто-нибудь из них захочет глотнуть воды, то он будет подходить к стакану брать его и после глотка сразу же ставить на место, да в этом случае уже нет оптимизации, но зато объем воды в стакане всегда актуальный и это избавляет нас от состояния гонки.

Ниже приведен пример кода, при помощи которого вы можете наблюдать результаты “состояние гонки”. При каждом запуске данного кода результаты будут непредсказуемы, хотя ожидалось увидеть цифру 2000. Проблема легко решается добавлением ключевого слова volatile в декларации переменной private volatile int volume = 0;

class Scratch < static class GlassOfWater < private int volume = 0; public int getVolume() < return volume; >public void setVolume(int volume) < this.volume = volume; >> public static void main(String[] args) throws InterruptedException < GlassOfWater glassOfWater = new GlassOfWater(); Thread thread1 = new Thread(() -> < for (int i = 0; i < 1000; i++) < glassOfWater.setVolume(glassOfWater.getVolume() + 1); >>); Thread thread2 = new Thread(() -> < for (int i = 0; i < 1000; i++) < glassOfWater.setVolume(glassOfWater.getVolume() + 1); >>); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Volume is " + glassOfWater.getVolume()); > > 

Взаимная блокировка

Взаимная блокировка (Deadlock) — это ситуация, когда два или более потока зацикленно ожидают друг друга, чтобы освободить ресурсы или завершить определенные операции. В результате ни один из потоков не может продолжить свое выполнение, так как каждый из них блокирует ресурсы, необходимые для завершения работы другого потока. Это приводит к тому, что программа останавливается и не может продолжить свое выполнение, пока ситуация deadlock не будет разрешена вручную.

Вернемся в ту же комнату, где нас ожидают Саша и Петя, и представим что в этой же комнате есть два чемоданчика, первый с различными инструментами типа плоскогубцы, молоток, всякие ключи и т.д, а во втором у нас крепеж, ну типа гайки, шурупы, гвозди, саморезы и т.д. Саша решил что-то отремонтировать в комнате и ему понадобились оба эти чемоданчика. И в то же время он хочет быть уверенным что никто ничего от туда не возьмет пока он работает, для этого он запереть на ключи нужные ему чемоданчики. Таким образом каждый раз когда он что-то будет брать или класть в чемоданчик он его отпирает и запирает.

Данный прием в Java называется блокировкой, это как ключ, который только один человек может держать в руках в определенный момент времени. Когда кто-то владеет ключом (блокирует доступ), другие люди должны ждать своей очереди. Блокировка гарантирует, что критическая секция кода будет выполняться только одним потоком за раз. Таким ключиком в языке Java является ключевое слово synchronized.

Ключевое слово synchronized обеспечивает атомарность операций и предотвращает конфликты между потоками. Это слово может быть применено как к методу целиком, так и к отдельному участку кода.

class MyClass < int counter; public synchronized void doSomething() < counter++; >> class MyClass < int counter; public void doSomething() < synchronized(this) < counter++; >> > 

С понятием synchronized мы разобрались и можем продолжить обзор того что же такое “Взаимная блокировка”.

Вернемся к Саше, сначала он взял ключ от ящика с инструментами и запер его, но в это же время Петя тоже решил заняться ремонтом и так же решил запереть чемоданчик, но первым он запер чемоданчик с крепежом. Далее Пете нужен чемоданчик с инструментами, он пошел к этому чемоданчику и видит что он уже заперт Сашей, “ну что же раз такое дело то подожду пока Саша завершит работу с ним” подумал Петя. В то же время Саше нужен чемоданчик с крепежом, но он так же видит что чемоданчик уже заперт Петей, в итоге Саша так же решил подождать. Мораль в том что они будут ждать до бесконечности, так и возникает эта самая взаимная блокировка.

Заключение

Мы рассмотрели лишь малую, но очень важную часть многопоточности в Java. Теперь, когда вы ознакомились с базовыми понятиями и это не отпугнуло вас, пришло время углубиться и познакомиться с другими, более продвинутыми механизмами синхронизации. Они включают в себя очереди, флаги и семафоры, которые позволяют координировать доступ и взаимодействие между потоками. Чтобы достичь максимальной производительности в многопоточных приложениях, недостаточно знать и использовать только ключевые слова. Важно также правильно управлять доступом к общим данным и предотвращать конфликты между потоками. Удачи!

  • Программирование
  • Java
  • Параллельное программирование

Что такое race condition?

Состояние гонки (race condition) — ошибка проектирования многопоточной системы или приложения, при которой эта работа напрямую зависит от того, в каком порядке выполняются потоки. Состояние гонки возникает, когда поток, который должен исполнится в начале, проиграл гонку и первым исполняется другой поток: поведение кода изменяется, из-за чего возникают недетерменированные ошибки.

Предыдущая статья В чем заключаются различия между CyclicBarrier и CountDownLatch?

Следующая статья Существует ли способ решения проблемы race condition?

ЭТО МОЖЕТ БЫТЬ ИНТЕРЕСНОЕЩЕ ОТ АВТОРА

Шифрование и расшифровка файла с помощью Java

Шифрование и расшифровка файла с помощью Java

Полиморфизм в java

Полиморфизм в java

Атака на облачные Java-приложения и их защита [видео Eng]

Атака на облачные Java-приложения и их защита [видео Eng]

Популярное

Шпаргалка Cisco CCNA (Cheat Sheet)

Рейтинги популярности языков программирования и СУБД в 2019 году

Что такое база данных?

Как выбрать идеальный хостинг для WordPress?

Горячее

Тёмная сторона работы в Яндекс.Маркете

Как я проработал в Яндекс.Маркете 15 месяцев. Тёмная сторона.

Подборка книг для дизайнера

Подборка книг для дизайнера, которые должен прочитать каждый

Как открыть банковскую карту в Беларуси почти бесплатно.

Как открыть банковскую карту в Беларуси

Задача на логику про 12 фальшивых монет

Задача на логику про 12 фальшивых монет

Выбор редактора

❤️�� Путь к мечте: как инвалидность не помешала стать разработчиком и.

Подборка облачных хранилищ и файлообменников

Создание повествования в играх, Теренс Ли

Популярные посты

Логическая задача про 51 рубль

Создаем многопользовательскую веб-игру Javascript

Топ самых сильных IT университетов в России 2023

ПОПУЛЯРНЫЕ КАТЕГОРИИ
  • Новости 191
  • Системный администратор 181
  • Видео 96
  • Программирование Java 86
  • Книги по программированию 66
  • Подборки 57
  • Frontend 46
  • Задачи 42
  • Переводы 29

Мы публикуем лекции и книги по программированию, видеоуроки, доклады с IT конференций. Разработка игр #Gamedev, создание и верстка сайтов, дизайн, уроки по схемотехнике, уроки по созданию приложений для IOS и Android и многое другое! C++, C#, Java, Objective‑C, Perl, Python, Ruby, PHP, Lua, Scala, Erlang, Haskell, Lisp, OCaml, Clojure, F#, Prolog, Delphi, VB, 1C, Smalltalk, Fortran, Matlab, Javascript, Asm.

Свяжитесь с нами: [email protected]

Следуйте за нами

© Мы публикуем лекции и книги по программированию, видеоуроки, доклады с IT конференций.

Состояние гонки (Race conditional)

Состоя́ние го́нки (англ. race condition) — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем (см. Гонки сигналов).

Состояние гонки — «плавающая» ошибка (гейзенбаг), проявляющаяся в случайные моменты времени и «пропадающая» при попытке её локализовать.

Рассмотрим пример кода (на Java).

volatile int x;

// Поток 1:while (!stop)

Пусть x=0. Предположим, что выполнение программы происходит в таком порядке:

    1. Оператор if в потоке 2 проверяет x на чётность.
    2. Оператор «x++» в потоке 1 увеличивает x на единицу.
    3. Оператор вывода в потоке 2 выводит «x=1», хотя, казалось бы, переменная проверена на чётность.

    Способы решения
    Локальная копия

    Самый простой способ решения — копирование переменной x в локальную переменную. Вот исправленный код:

    Естественно, этот способ работает только тогда, когда переменная одна и копирование производится за одну машинную команду.

    Синхронизация

    Более сложный, но и более универсальный метод решения — синхронизация потоков, а именно:

    Здесь семантика happens before не требует ключевое слово volatile

    Комбинированный способ

    Предположим, что переменных — две (и ключевое слово volatile не действует), а во втором потоке вместо System.out.println стоит более сложная обработка. В этом случае оба метода неудовлетворительны: первый — потому что одна переменная может измениться, пока копируется другая; второй — потому что засинхронизирован слишком большой объём кода.

    Эти способы можно скомбинировать, копируя «опасные» переменные в синхронизированном блоке. С одной стороны, это снимет ограничение на одну машинную команду, с другой — позволит избавиться от слишком больших синхроблоков.

    volatile int x1, x2;

    Очевидных способов выявления и исправления состояний гонки не существует. Лучший способ избавиться от гонок — правильное проектирование многозадачной системы.

    Случай с Therac-25

    Аппарат лучевой терапии Therac-25 был первым в США медицинским аппаратом, в котором вопросы безопасности были возложены исключительно на программное обеспечение.

    Из-за состояния гонки между управляющей программой и обработчиком клавиатуры иногда случалось, что пациент напрямую облучался пучком электронов в 25 МэВ, что вело к переоблучению. При этом датчики выводили «Нулевая доза», поэтому оператор мог повторить процедуру, усугубляя ситуацию. В результате погибли как минимум четыре пациента (подробнее в викепедии)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *