Какое ключевое слово используется для получения блокировки монитора объекта
Модель памяти, существующая на данный момент в Java, гарантирует ожидаемый порядок выполнения многопоточного кода, при отсутствии в этом коде гонок потоков. И для того, чтобы обезопасить ваш код от гонок, придуманы различные способы синхронизации и обмена данными между ними.
- Atomic
- Locks
- Collections
- Synchronization points
- Executors
- Accumulators _jdk 1.8_
Atomic
В дочернем пакете java.util.concurrent.atomic находится набор классов для атомарной работы с примитивными типами. Контракт данных классов гарантирует выполнение операции compare-and-set за «1 единицу процессорного времени». При установке нового значения этой переменной вы также передаете ее старое значение (подход оптимистичной блокировки). Если с момента вызова метода значение переменной отличается от ожидаемого — результатом выполнения будет false .
Для примера возьмем два массива long переменных [1,2,3,4,5] и [-1,-2,-3,-4,-5] . Каждый из потоков будет последовательно итерироваться по массиву и суммировать элементы в единую переменную. Код (groovy) с пессимистичной блокировкой выглядит так:
Результат выполнения будет ожидаемым:
- попытка блокирования монитора
- блокировка потока
- разблокировка монитора
- разблокировка потока
Рассмотрим использование AtomicLong для реализации оптимистичной блокировки при расчете этой же суммы:
Как видно из результатов «ошибочных» попыток было не так уж и много:
При решении использовать оптимистичную блокировку важно, чтобы действие с модифицируемой переменной не занимало много времени. Чем дольше это действие — тем чаще будут случаться ошибочные compare-and-set , и тем чаще придется выполнять это действие повторно.
На основе compare-and-set может также реализовываться неблокирующая read блокировка. В данном случае в atomic переменной будет храниться версия обрабатываемого объекта. Получив значение версии до вычислений мы можем сверить ее после вычисления. Обычные read-write блокировки вступают в силу, только если проверка версии провалилась.
Locks
ReentrantLock
В отличие от syncronized блокировок, ReentrantLock позволяет более гибко выбирать моменты снятия и получения блокировки т.к. использует обычные Java вызовы. Также ReentrantLock позволяет получить информацию о текущем состоянии блокировки, разрешает «ожидать» блокировку в течение определенного времени. Поддерживает правильное рекурсивное получение и освобождение блокировки для одного потока. Если вам необходимы честные блокировки (соблюдающие очередность при захвате монитора) — ReentrantLock также снабжен этим механизмом.
Несмотря на то, что syncronized и ReentrantLock блокировки очень похожи — реализация на уровне JVM отличается довольно сильно.
Не вдаваясь в подробности JMM: использовать ReentrantLock вместо предоставляемой JVM syncronized блокировки стоит только в том случае, если у вас очень часто происходит битва потоков за монитор. В случае, когда в syncronized метод _обычно_ попадает лишь один поток — производительность ReentrantLock уступает механизму блокировок JVM.
ReentrantReadWriteLock
Дополняет свойства ReentrantLock возможностью захватывать множество блокировок на чтение и блокировку на запись. Блокировка на запись может быть «опущена» до блокировки на чтение, если это необходимо.
StampedLock _jdk 1.8_
Реализовывает оптимистичные и пессимистичные блокировки на чтение-запись с возможностью их дальнейшего увеличения или уменьшения. Оптимистичная блокировка реализуется через «штамп» лока (javadoc):
Collections
ArrayBlockingQueue
ConcurrentHashMap
Ключ-значение структура, основанная на hash функции. Отсутствуют блокировки на чтение. При записи блокируется только часть карты (сегмент). Кол-во сегментов ограничено ближайшей к concurrencyLevel степени 2.
ConcurrentSkipListMap
Сбалансированная многопоточная ключ-значение структура (O(log n)). Поиск основан на списке с пропусками. Карта должна иметь возможность сравнивать ключи.
ConcurrentSkipListSet
ConcurrentSkipListMap без значений.
CopyOnWriteArrayList
Блокирующий на запись, не блокирующий на чтение список. Любая модификация создает новый экземпляр массива в памяти.
CopyOnWriteArraySet
CopyOnWriteArrayList без значений.
DelayQueue
PriorityBlockingQueue разрешающая получить элемент только после определенной задержки (задержка объявляется через Delayed интерфейс объекта). DelayQueue может быть использована для реализации планировщика. Емкость очереди не фиксирована.
LinkedBlockingDeque
Двунаправленная BlockingQueue , основанная на связанности (cache-miss & cache coherence overhead). Емкость очереди не фиксирована.
LinkedBlockingQueue
Однонаправленная BlockingQueue , основанная на связанности (cache-miss & cache coherence overhead). Емкость очереди не фиксирована.
LinkedTransferQueue
Однонаправленная `BlockingQueue`, основанная на связанности (cache-miss & cache coherence overhead). Емкость очереди не фиксирована. Данная очередь позволяет ожидать когда элемент «заберет» обработчик.
PriorityBlockingQueue
SynchronousQueue
Однонаправленная `BlockingQueue`, реализующая transfer() логику для put() методов.
Synchronization points
CountDownLatch
Барьер ( await() ), ожидающий конкретного (или больше) кол-ва вызовов countDown() . Состояние барьера не может быть сброшено.
CyclicBarrier
Барьер ( await() ), ожидающий конкретного кол-ва вызовов await() другими потоками. Когда кол-во потоков достигнет указанного будет вызван опциональный callback и блокировка снимется. Барьер сбрасывает свое состояние в начальное при освобождении ожидающих потоков и может быть использован повторно.
Exchanger
Барьер (`exchange()`) для синхронизации двух потоков. В момент синхронизации возможна volatile передача объектов между потоками.
Phaser
Расширение `CyclicBarrier`, позволяющая регистрировать и удалять участников на каждый цикл барьера.
Semaphore
Барьер, разрешающий только указанному кол-во потоков захватить монитор. По сути расширяет функционал `Lock` возможность находиться в блоке нескольким потокам.
Executors
ExecutorService пришел на замену new Thread(runnable) чтобы упростить работу с потоками. ExecutorService помогает повторно использовать освободившиеся потоки, организовывать очереди из задач для пула потоков, подписываться на результат выполнения задачи. Вместо интерфейса Runnable пул использует интерфейс Callable (умеет возвращать результат и кидать ошибки).
Метод invokeAll отдает управление вызвавшему потоку только по завершению всех задач. Метод invokeAny возвращает результат первой успешно выполненной задачи, отменяя все последующие.
ThreadPoolExecutor
Пул потоков с возможностью указывать рабочее и максимальное кол-во потоков в пуле, очередь для задач.
ScheduledThreadPoolExecutor
Расширяет функционал ThreadPoolExecutor возможностью выполнять задачи отложенно или регулярно.
ThreadPoolExecutor
Более легкий пул потоков для «самовоспроизводящих» задач. Пул ожидает вызовов `fork()` и `join()` методов у дочерних задач в родительской.
Блокировка на уровне объекта
Это механизм синхронизации не статического метода или не статического блока кода, такой, что только один поток сможет выполнить данный блок или метод на данном экземпляре класса. Это нужно делать всегда, когда необходимо сделать данные на уровне экземпляра потокобезопасными. Пример: или или
Блокировка на уровне класса
Предотвращает возможность нескольким потокам войти в синхронизированный блок во время выполнения в любом из доступных экземпляров класса. Это означает, что если во время выполнения программы имеется 100 экземпляров класса DemoClass , то только один поток в это время сможет выполнить demoMethod() в любом из случаев, и все другие случаи будут заблокированы для других потоков. Это необходимо когда требуется сделать статические данные потокобезопасными. или или
Бывают случаи, когда два или более параллельно-выполняемых потока пытаются обратиться к общему ресурсу. Если ресурс может быть изменен в результате выполнения одного из потоков, то другие потоки должны дождаться пока изменения в потоке будут завершены. В противном случае, потоки получат ресурс, данные которого будут ошибочными.
Поток выполнения (который представлен объектом) может завладеть монитором в случае, если он запросил блокировку и монитор свободен на данный момент. После того, как объект вошел в монитор, все остальные объекты-потоки, пытающиеся войти в монитор, приостанавливаются и ожидают до тех пор, пока первый объект не выйдет из монитора.
Монитором может обладать только один поток. Если поток (объект) обладает монитором, то он при необходимости может повторно войти в него.
В языке Java синхронизация применяется к целым методам или фрагментам кода. Исходя из этого существует два способа синхронизации программного кода:
- за счет использования модификатора доступа synchronized ;
- за счет использования оператора synchronized () <> .
2. Модификатор доступа synchronized . Общая форма
Модификатор доступа synchronized применяется при объявлении синхронизированного метода и имеет следующую общую форму:
- MethodName – имя метода, который есть общим ресурсом и его нужно синхронизировать;
- return_type -тип, который возвращает метод;
- parameters – параметры метода.
3. Оператор synchronized() < >. Общая форма
Оператор synchronized() , в отличие от модификатора доступа synchronized , используется для применения синхронизации к объектам (методам), которые изначально при их разработке не предназначались для многопоточного доступа. То есть, в классе не реализованы методы с модификатором доступа synchronized (классы сторонних разработчиков).
Общая форма оператора synchronized () следующая:
- reference – ссылка на синхронизируемый объект;
- фигурные скобки <> , которые определяют блок синхронизации. В этом блоке указываются операторы для синхронизации.
4. Пример, демонстрирующий синхронизированный доступ к общему методу из трех разных потоков. Применение модификатора доступа synchronized
В примере демонстрируется необходимость применения модификатора доступа synchronized с целью упорядочения доступа к ресурсу из разных потоков.
Общим ресурсом является метод Get() класса Array5. Метод Get() возвращает массив из 5 чисел, значения которых последовательно изменяются от 1 к 5. К этому методу будет осуществлена попытка одновременного доступа из разных потоков, поэтому метод обозначен как synchronized .
Инкапсуляция одного потока выполнения осуществляется в классе ArrayThread классическим (стандартным) способом с помощью реализации интерфейса Runnable . Класс ArrayThread содержит следующие общедоступные ( public ) элементы:
В классе Threads представлена функция main() , в которой создается три потока AT1 , AT2 , AT3 типа ArrayThread и один объект A5 . Метод Get() объекта A5 является общим ресурсом для этих трех потоков.
Результат выполнения программы
Если в вышеприведенном примере перед методом Get() класса Array5 убрать ключевое слово synchronized
то последовательного выполнения потоков не будет. В этом случае программа после каждого запуска будет выдавать разный (хаотический) результат, например следующий
5. Пример использования оператора synchronized() <> для синхронизированного доступа к общему ресурсу
Код предыдущего примера (смотрите п. 4), синхронизирующего потоки, можно представить по-другому. В этом случае, вместо модификатора доступа synchronized перед именем метода Get() нужно использовать оператор synchronized () <> . Оператор synchronized должен быть использован в методе run() класса ArrayThread .
Ключевое слово Java synchronized используется в многопоточности для создания блока кода, который может выполняться только одним потоком одновременно.
Зачем нам нужна синхронизация?
Когда у нас есть несколько потоков, работающих над общим объектом, конечный результат может быть поврежден. Допустим, у нас есть простая программа для увеличения переменной счетчика объекта. Эта переменная является общей для всех потоков.
Пример синхронизированного ключевого слова Java
Как синхронизированное ключевое слово работает внутри компании?
Логика синхронизации Java построена вокруг внутренней сущности, называемой встроенной блокировкой или блокировкой монитора .
Когда поток пытается войти в синхронизированную область, он должен сначала получить блокировку объекта. Затем выполняются все операторы в синхронизированном блоке. Наконец, поток освобождает блокировку объекта, который может быть получен другими потоками в пуле ожидания.
Блок синхронизации Java
Когда блок кода обернут вокруг ключевого слова synchronized, он называется синхронизированным блоком.
Синтаксис синхронизированного блока
Вот простой пример синхронизированного блока в Java.
Метод синхронизации Java
Иногда требуется синхронизировать каждый оператор внутри метода. В этом случае мы можем синхронизировать сам метод.
Синтаксис синхронизированного метода
Вот пример метода синхронизации java.
Блокировка объекта в Синхронизированном методе
Как и синхронизированный блок, синхронизированные методы также требуют блокировки объекта.
- Если метод статический , блокировка приобретается для класса.
- Если метод нестатичен, блокировка будет получена для текущего объекта.
Метод синхронизации Java против блока
- Метод синхронизации Java блокирует текущий объект, поэтому, если существует другой синхронизированный метод, другие потоки будут ожидать блокировки объекта, даже если в этих методах нет общей переменной. Синхронизированный блок Java работает с полем объекта, поэтому в этом случае лучше использовать синхронизированный блок.
- Если объект имеет несколько синхронизированных методов, работающих с одними и теми же переменными, то предпочтителен синхронизированный метод. Например, StringBuffer использует синхронизированные методы, поскольку все методы append() и insert() работают с одним и тем же объектом.
Вот пример, когда у нас есть несколько методов, работающих с одной и той же переменной, поэтому использование синхронизированного метода является лучшим выбором.
Вот еще один пример, когда различные методы работают с другой общей переменной, поэтому использование синхронизированного блока является лучшим выбором.
Вывод
Ключевое слово Java synchronized полезно для предотвращения повреждения данных при многопоточном программировании. Однако синхронизация снижает производительность кода из-за дополнительных накладных расходов на механизм блокировки. Использование синхронизированного блока или синхронизированного метода во многом зависит от требований вашего проекта.
Читайте также: