Практика многопоточности в Java

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

Если вы еще не читали часть 1, прочтите ее здесь и исследуйте примеры в этой статье в совершенно новом свете.



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

СОДЕРЖАНИЕ

  1. Задача Fizz-Buzz с четырьмя потоками.
  2. Печать четно-нечетных серий с семафорами
  3. Проблема производителя и потребителя
  4. Обеденная проблема философа

Задача Fizz-Buzz с четырьмя потоками.

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

Задача

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

  1. Выведите Fizz, если число делится на 3, с первым потоком.
  2. Выведите Buzz, если число делится на 5, со второй цепочкой.
  3. Выведите Fizz-Buzz, если делится на оба с третьей нитью.
  4. Выведите только число, если ничего из вышеперечисленного не применимо к четвертому потоку.

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

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

Подход

Здесь нам нужно использовать четыре потока. Первый выводит Fizz, если он делится на 3. Второй поток печатает Buzz, если он делится на 5. Третий поток печатает Fizz-Buzz, если делится на оба 3 и 5. Четвертый поток просто напечатает число , если ничего из вышеперечисленного не выполняется.

Здесь все потоки имеют общий доступ к задаче объекта, к которой один из потоков сможет получить доступ в любой момент. Переменная isTurn будет определять, какой поток должен получить монитор для объекта задачи, чтобы распечатать соответствующее сообщение.

Здесь мы дадим четвертому потоку фору. Обратите внимание, что объект задачи используется всеми потоками. Переменная isTurn используется, чтобы указать, какой поток должен печатать следующим на основе следующего числа, определенного методом getNextNum (int n).

Сначала четвертый поток напечатает 1, а затем следующий, поскольку 2 не делится на 3 или 5, четвертый поток напечатает 2. Затем, когда номер равен 3, первый поток получит доступ и напечатает Fizz. Тот же самый образец повторяется до 25, что является нашим пределом здесь.

Результат будет выглядеть так.

Немного о изменчивом ключевом слове

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

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

С использованием ключевого слова volatile изменения в одном потоке видны другому. Более подробное объяснение ключевого слова volatile можно найти здесь и здесь.

Печать четно-нечетных серий с семафорами

Мы уже решили эту проблему в Части 1 с использованием synchronized, wait () и notify (). Давайте решим это с помощью семафоров. Но прежде чем погрузиться в подробности, давайте разберемся, что же такое семафор.

Семафор

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

Подход к проблеме

Здесь мы используем семафоры для синхронизации потоков. В то время как поток 1 печатает нечетное число, поток 2 должен ждать, и наоборот. Это будет продолжаться до тех пор, пока цикл не закончится.

У нас нет переменной isTurn, чтобы решить, какой поток должен печатать число, вместо этого у нас есть два семафора - нечетный и четный.

Концептуально семафор поддерживает набор разрешений. Здесь мы даем «толчок» нечетному потоку, давая ему разрешение.

Теперь нам нужно понять, что такое методы collect () и release () в Semaphore.

Метод Acqua () блокирует, если разрешение недоступно, в противном случае он позволяет уменьшить количество доступных разрешений на единицу.

Метод release () добавляет разрешение к семафору.

Здесь, когда нечетный семафор вызывает метод acqu (), поскольку у него уже есть разрешение, поток не будет заблокирован и распечатает нечетное число.

В то же время, когда даже поток вызывает метод acqu (), он будет заблокирован, так как у него нет разрешения. Когда нечетный поток завершает печать, он добавит разрешение на четный семафор, который предоставит доступ четному потоку для печати.

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

Результат будет выглядеть так.

Проблема производителя и потребителя

Это одна из классических проблем многопоточности. Есть одна очередь вместимостью n. Эта очередь разделяется между производителем и потребителем. Здесь n равно 5.

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

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

Здесь производитель получит преимущество, так как очередь пуста, а потребитель будет ждать своей очереди, пока производитель не закончит производство в очереди.

Как только производитель закончит, потребитель начнет потреблять, а производитель будет ждать, пока очередь не опустеет. Это будет продолжаться в зависимости от условия в цикле while.

Не стесняйтесь экспериментировать, так как существует множество версий этой проблемы. Попробуйте это с несколькими потребителями и поэкспериментируйте с логикой ожидания.

Вывод

Обеденная проблема философа

Это еще одна классическая проблема многопоточности и синхронизации потоков.

Заявление и объяснение

Здесь пять философов сидят за обеденным столом с пятью тарелками и пятью вилками, как показано на изображении ниже. Все, что они делают, это думают и едят.

Здесь вилки - это общие объекты. Каждая вилка используется двумя философами.

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

Если это произойдет, никто не сможет есть и будет ждать бесконечно.

Здесь нам нужно создать пять потоков, по одной для каждого философа. Также нам нужно создать пять объектов вилки.

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

в чем может быть причина?

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

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

Здесь мы используем синхронизированный блок для доступа к монитору. В предыдущих примерах мы использовали синхронизированный метод.

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

Так будет продолжаться бесконечно. Поэкспериментируйте с кодом и определите случайное время сна.

Вывод

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

Вывод

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

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

Рекомендуемая литература

  1. Параллелизм Java на практике
  2. Обедающая проблема философа с использованием семафоров
  3. Https://medium.com/the-glitcher/multi-threading-with-java-eb3d092d58af
  4. Https://www.baeldung.com/java-concurrency