Что такое монитор java

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

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

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

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

Java Multithreading часть 2. Что такое монитор

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

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

Свой отпечаток на написание параллельных программ оставляет наш последовательный образ мышления людей как биологического вида. Люди – однопоточные. Сделаю небольшую ремарку. В эпоху одноядерных процессоров так же было возможно “параллельное” выполнение за счет работы потоков операционной системы.

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

Thread

Начиная с версии Java 1.0 в пакете java.lang есть специальный класс, который позволяет создавать новые потоки (или треды) – Thread. Этот класс реализует интерфейс Runnable – специальный интерфейс, который является функциональным, начиная с версии Java 8, и содержит один метод, в котором должна быть описана задача потока – то что он будет делать:

Java. Многопоточность. Урок 17. Synchronized, монитор, синхронизированные блоки.

Источник: nicholasgribanov.name

Еще по теме:  W1942s характеристики монитор LG flatron

Сравнение механизмов синхронизации потоков в Java

Выполнение нескольких параллельных потоков требует особого внимания, когда они обращаются (выполняют операции чтения и записи) к одному и тому же блоку памяти. Фрагмент кода, в котором происходит этот доступ к памяти, называется критическим разделом, и доступ к потоку должен быть синхронизирован, чтобы избежать состояния гонки. В Java синхронизировать доступ к критическим разделам можно с помощью различных механизмов: Монитор, Блокировка и Семафор.

В этой статье я проведу тщательное сравнение этих механизмов, пытаясь синхронизировать известную проблему «производитель-потребитель». В следующем коде показана базовая реализация структуры данных Buffer. Мы попытаемся синхронизировать доступ к обоим методам addItem и getItem , вызываемым из разных параллельных потоков.

Чтобы смоделировать сценарий, я создал два Thread класса, а именно Producer и Consumer , каждый из которых будет производить и потреблять 10 элементов соответственно. Основной код приложения создает один экземпляр Buffer и запускает несколько потоков типа Producer и Consumer , предоставляя им ссылку на один и тот же экземпляр Buffer — таким образом, все потоки будут пытаться получить доступ к одной и той же памяти. Следующий фрагмент демонстрирует основную логику приложения и выходные данные для несинхронизированного выполнения, в то время как следующие примеры будут сосредоточены исключительно на коде Buffer , поскольку мы будем синхронизировать доступ к методам с различными механизмами.

Output: The item «1 produced by: Producer4» consumed by Consumer6 The item «2 produced by: Producer6» consumed by Consumer6 The item «0 produced by: Producer9» consumed by Consumer1 The item «0 produced by: Producer4» consumed by Consumer1 Producer5 produces item 0 The item «null» consumed by Consumer1 The item «0 produced by: Producer6» consumed by Consumer9 The item «null» consumed by Consumer9 The item «null» consumed by Consumer8 The item «null» consumed by Consumer8 .

Синхронизация с помощью монитора буфера:

Мьютекс — это конструкция, которая используется для защиты общих данных таким образом, что позволяет только одному потоку получить доступ к этой общей области. С другой стороны, у каждого объекта Java есть монитор, который состоит из мьютекса и некоторых общих переменных, и поэтому он предоставляет три метода — wait() , notify() и notifyAll() . Эти методы можно использовать для приостановки и уведомления потоков до тех пор, пока какое-либо условие не станет истинным.

Ключевой вывод — использование ключевого слова synchronized , которое можно использовать на уровне метода или внутри метода объекта.

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

Оба метода, getItem и addItem , определены как synchronized , что означает, что любой Thread , который вызовет любой из них, получит монитор объекта, а любой последующий Thread , который может вызывать любой метод, будет заблокирован до тех пор, пока монитор не будет освобожден. Теперь помните, поток, который контролирует монитор, может освободить его после выполнения синхронизированного метода, или вызвав this.wait() , или просто wait() . Этот метод блокирует вызывающий поток и освобождает монитор, который будет захвачен другим ожидающим потоком.

Еще по теме:  Какое разрешение экрана лучше для монитора

В нашем случае Consumer может потреблять, только если в буфере есть хотя бы один элемент — поэтому мы блокируем каждый Consumer поток до тех пор, пока не появятся элементы, добавленные Producer потоками.

Точно так же Producer может выдавать, только если буфер не заполнен — ​​поэтому мы блокируем каждый Producer поток до тех пор, пока в буфере не останется свободного места. После использования элемента или создания элемента текущий поток вызывает notifyAll() , который уведомляет все ожидающие потоки о попытке получить монитор объекта и может выполнить соответствующее действие.

Что произойдет в сценарии, когда два потока Consumer заблокированы (в режиме ожидания), а один поток Producer уведомляет все ожидающие потоки после того, как он создает только один элемент?

Очевидно, что оба Consumer потока могут продолжать использовать элементы без дополнительной проверки. Но если один из этих потоков получил элемент, условие становится недопустимым. Как затем заставить каждый ожидающий поток выполнить проверку условий (пуст ли буфер для Consumer и заполнен ли он для Producer )? Просто заменим if на while , и оба условия станут следующими:

while(items.size() == 0) wait(); ——- while(items.size() == capacity) wait();

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

Output: Producer7 produces item 5 Producer9 produces item 2 The item «5 produced by: Producer7» consumed by Consumer1 The item «0 produced by: Producer6» consumed by Consumer5 The item «1 produced by: Producer6» consumed by Consumer2 Producer5 produces item 2 The item «0 produced by: Producer3» consumed by Consumer4 The item «1 produced by: Producer7» consumed by Consumer2

Синхронизация с помощью блокировок:

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

Следующий код показывает синхронизацию Buffer с использованием экземпляра ReentrantLock .

  • Consumer должен потреблять предмет, когда он доступен, и если он когда-либо станет доступным. Вот почему каждый Consumer зацикливается, пока не сможет заблокировать доступ к общему ресурсу и попытаться его использовать. Если он не готов к использованию, то есть в буфере нет элементов, он просто снимает блокировку, надеясь, что поток Producer получит ее следующим, чтобы создать элемент, и снова зацикливается, чтобы попытаться выполнить ту же задачу. Цикл заканчивается, когда элемент добавляется в буфер, и потребитель должен гарантировать снятие уже полученной блокировки, разблокировав ресурс, прежде чем он выйдет из метода.
  • Producer должен создать элемент, если буфер не заполнен, в противном случае он должен ждать, пока в буфере не освободится какое-то пространство. Он выполняет блокировку ресурсов аналогично потребителю.

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

Синхронизация с использованием семафоров

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

Еще по теме:  Монитор LG w1943st характеристики

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

Нам нужно 2 Semaphores для синхронизации getItem и addItem соответственно. Потоки Producer будут уведомлять потоки Consumer , когда элемент добавляется в буфер, а потоки Consumer будут уведомлять потоки Producer , когда элемент удаляется из буфера.

Следовательно, produceSemaphore будет инициализирован с одним разрешением, чтобы позволить Producer вставить элемент в пустой буфер, в то время как consumeSemaphore будет инициализирован без разрешений, поскольку Consumer не может потреблять из пустого буфера.

Затем Consumer может быть готов к потреблению и получению разрешения от consumeSemaphore , однако буфер может быть пуст. Таким образом, нам нужно заблокировать этот поток до тех пор, пока элемент не будет создан Producer . Мы делаем это с помощью другого семафора, psCoordinatorSemaphore , который будет действовать как мьютекс — в этом случае мы также можем использовать простую блокировку.

Ожидание выполняется с помощью следующего кода:

pcCoordinatorSemaphore.acquire(); while (items.size() == 0) < pcCoordinatorSemaphore.release(); Thread.sleep(100); pcCoordinatorSemaphore.acquire(); >

Очень важно понимать эту часть кода:

Вызывающий поток получит разрешение от pcCoordinatorSemaphore и будет постоянно проверять, есть ли элементы в буфере. Если нет, он освобождает разрешение, надеясь, что оно будет в следующий раз получено потоком Producer (для моделирования я помещаю потребителя в спящий режим на 100ms , чтобы дать потоку Producer больше времени для получения разрешения).

Но до истечения этого времени он также должен снова получить разрешение на проведение проверки. То же самое объяснение применимо и к методу addItem .

Спасибо, что прочитали эту статью, надеюсь, вы найдете ее полезной.

Источник: skine.ru

Несколько способов синхронизации процессов и потоков в Java

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

Критическая секция

  • параллельные потоки работают с общим ресурсом;
  • требуется синхронизация между потоками, а не процессами;

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

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

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

Пример

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

Работа программы до использования блока синхронизации

Работа программы до использования блока синхронизации

Оцените статью
Добавить комментарий