Гонка потоков java пример

Гонка потоков java пример

В предыдущей статье мы рассмотрели базовые темы многопоточности: потоки, класс Thread и интерфейс Runnable. В данной публикации продолжаем осваивать эту непростую тему, разбирая такие понятия, как процесс, проблема видимости, volatile, состояние гонки и многое другое.

Процесс — это совокупность ресурсов (кода, виртуального адресного пространства, открытых файлов, уникального идентификатора PID и т. д.), выделенных операционной системой для выполнения запускаемого приложения.

У каждого процесса имеется хотя бы один поток, называемый главным, с которого начинается выполнение программы. В Java после создания процесса работа главного потока стартует с метода main(). Затем в заданных программистом местах запускаются другие потоки.

Для нужд процесса выделяется необходимый объем памяти из оперативной памяти (в Java — из пространства кучи) в границах которой и происходит обработка данных приложения, а для каждого потока — свой стек и регистры. При этом сам процесс не исполняет код программы — этим занимаются потоки, создаваемые внутри него.

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

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

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

Читайте также:  JSoup Example

Считается, что процессы являются достаточно ресурсоемкими сущностями, а подпроцессы — их облегченным вариантом или легковесными процессами.

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

 public class Playground < private static boolean running; public static void main(String[] args) < var t1 = new Thread(() -> < while (!running) <>System.out.println("topjava.ru"); >); var t2 = new Thread(() -> < running = true; System.out.println("I love"); >); t1.start(); t2.start(); > > 

В приведенном выше фрагменте создаются два потока с общей переменной boolean running. Внутри первого потока while будет продолжать выполняться до тех пор, пока переменная имеет значение false. Как только он прервется, напечатается topjava.ru. Второй поток изменяет переменную running, а затем печатает I love.

Возникает вопрос, что в итоге будет выведено на консоль? Можно предположить следующий вариант (пусть это будет Ситуация 1):

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

Разберем другие возможные варианты работы потоков программы, что, несомненно, повлияет на порядок вывода текста в консоль.

Ситуация 2. Второй поток запускается первым и меняет значение переменной. В первом потоке прерывается цикл и отображается topjava.ru. А второй поток печатает I love.

Ситуация 3. Первый поток может застрять, а второй напечатает I love. И на этом все, больше никакого вывода. Это трудновоспроизводимая ситуация, но она может произойти.

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

Читайте также:  Helvetica neue cyr css

Когда процессор выполняет код, он считывает данные из основной памяти. Однако современные процессоры имеют различные кэши для более быстрого доступа к памяти: L1 Cache, L2 Cache и L3 Cache — в этом и кроется проблема из третьей ситуации.

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

Чтобы этого не происходило, мы можем запретить процессору кэшировать значение переменной, используя модификатор volatile. Вместо этого он будет считывать ее из основной памяти:

 public class FoojayPlayground
  • L — локальная переменная, например, L1, L2
  • S — ссылочная переменная, например, S1, S2. Видна нескольким потокам, может быть статической
  • S.X — S является ссылочной переменной, а X — полем объекта

В псевдокоде мы будем использовать номер потока и номер строки. Например, 1.1, где 1 — ID потока, а число после точки — номер строки. Или 1.2 — поток 1, строка 2.

 1) 1.1, 2.1, 1.2, 2.2 2) 1.1, 2.1, 2.2, 1.2 3) 1.1, 2.1, 2.2 

 1) 1.1, 1.2, 1.3, 2.1, 2.2, 2.3 2) 2.1, 2.2, 2.3, 1.1, 1.2, 1.3 

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

Ответ заключается в том, что хоть мы и пишем код в определенной последовательности, но это не означает, что компилятор и виртуальная машина выполнят его в заданном порядке. Они легко могут изменить его в качестве оптимизации (или вообще удалить, как «мертвый код»), если решат, что выходные данные не изменятся. Например, если в первом потоке поменять местами 1.1 и 1.2, то это не повлияет на конечный результат.

Читайте также:  Java annotation not null

Такого рода вмешательства происходят по разным причинам. Например, интеллектуальный алгоритм компилятора может найти способ оптимизировать конкретный код для более быстрого выполнения. Но нужно понимать, что эти изменения могут стать потенциальным источником багов, которые не всегда легко обнаружить. У этого вида ошибок есть даже свое название — Heisenbugs (близкий по значению термин — «плавающая ошибка»). Баги данного типа могут исчезать при их поиске и появляться в произвольные моменты времени.

Источник

Оцените статью