в чем смысл использования reentrantreadwritelock
Многопоточное программирование в Java 8. Часть вторая. Синхронизация доступа к изменяемым объектам
Авторизуйтесь
Многопоточное программирование в Java 8. Часть вторая. Синхронизация доступа к изменяемым объектам
Большинство принципов, которые описаны в этой статье, справедливы и для более старых версий Java. Тем не менее, я не задавался проблемами совместимости, и примеры содержат как лямбды, так и новые возможности многопоточности. Если вы не очень хорошо знакомы с лямбда-выражениями, то вам стоит сперва прочитать мой туториал по Java 8.
Синхронизация
Мы уже узнали, как выполнять код параллельно с помощью сервисов-исполнителя (ExecutorService). Во время написания многопоточной программы нужно уделять особое внимание работе с общими для потоков изменяемыми объектами. Давайте представим, что мы хотим увеличить такую переменную на единицу.
Если мы будем вызывать этот метод одновременно из двух потоков, у нас возникнут серьёзные проблемы:
Вместо ожидаемого постоянного результата 10000 мы будем каждый раз получать разные числа. Причина этого — использование изменяемой переменной несколькими потоками без синхронизации, что вызывает состояние гонки (race condition).
Увеличение числа на единицу происходит в три шага: (1) считать значение переменной, (2) увеличить это значение на единицу и (3) записать назад новое значение. Если два потока будут одновременно выполнять эти шаги, то вполне вероятно, что они могут выполнить первый шаг одновременно, считав одно и то же значение. Затем они запишут в переменную одно и то же значение, и вместо увеличения на 2 получится увеличение на единицу. Поэтому конечное значение и получается меньше ожидаемого.
Тогда, после выполнения метода 10000 раз, мы всегда будем получать значение 10000, и никакой гонки состояний возникать не будет:
Это ключевое слово можно применять не только к методам, но и к отдельным их блокам:
Под капотом Java использует так называемый монитор (monitor lock, intrinsic lock) для обеспечения синхронизации. Этот монитор привязан к объекту, поэтому синхронизированные методы используют один и тот же монитор соответствующего объекта. Все неявные мониторы устроены реентерабельно (reentrant), т.е. таким образом, что поток может без проблем вызывать блокировку одного и того же объекта, исключая взаимную блокировку (например, когда синхронизированный метод вызывает другой синхронизированный метод на том же объекте).
Блокировки
ReentrantLock
Класс ReentrantLock реализует то же поведение, что и обычные неявные блокировки. Давайте попробуем переписать наш пример с увеличением на единицу с помощью него:
Для большего контроля явные блокировки поддерживают множество специальных методов:
Пока первый поток удерживает блокировку, второй выведет следующую информацию:
Locked: true
Held by me: false
Lock acquired: false
ReadWriteLock
Интерфейс ReadWriteLock предлагает другой тип блокировок — отдельную для чтения, и отдельную для записи. Этот интерфейс был добавлен из соображения, что считывать данные (любому количеству потоков) безопасно до тех пор, пока ни один из них не изменяет переменную. Таким образом, блокировку для чтения (read-lock) может удерживать любое количество потоков до тех пор, пока не удерживает блокировка для записи (write-lock). Такой подход может увеличить производительность в случае, когда чтение используется гораздо чаще, чем запись.
В примере выше мы можем видеть, как поток блокирует ресурсы для записи, после чего ждёт одну секунду, записывает данные в HashMap и освобождает ресурсы. Предположим, что в это же время были созданы ещё два потока, которые хотят получить из хэш-таблицы значение:
Если вы попробуете запустить этот пример, то заметите, что оба потока, созданные для чтения, будут простаивать секунду, ожидая завершения работы потока для записи. После снятия блокировки они выполнятся параллельно, и одновременно запишут результат в консоль. Им не нужно ждать завершения работы друг друга, потому что выполнять одновременное чтение вполне безопасно (до тех пор, пока ни один поток не работает параллельно на запись).
StampedLock
Вот таким образом следовало бы переписать наш предыдущий пример под использование StampedLock :
В следующем примере демонстрируется «оптимистичная блокировка»:
Optimistic Lock Valid: true
Write Lock acquired
Optimistic Lock Valid: false
Write done
Optimistic Lock Valid: false
Оптимистичная блокировка является валидной с того момента, как ей удалось захватить ресурс. В отличии от обычных блокировок для чтения, оптимистичная не запрещает другим потокам блокировать ресурс для записи. Что же происходит в коде выше? После захвата ресурса блокировка является валидной и оптимистичный поток отправляется спать. В это время другой поток блокирует ресурсы для записи, не дожидаясь окончания работы чтения. Начиная с этого момента, оптимистичная блокировка перестаёт быть валидной (даже после окончания записи).
Таким образом, при использовании оптимистичных блокировок вам нужно постоянно следить за их валидностью (проверять её нужно уже после того, как выполнены все необходимые операции).
В этом примере мы хотим прочитать значение переменной count и вывести его в консоль. Однако, если значение равно нулю, мы хотим изменить его на 23. Для этого нужно выполнить преобразования из readLock во writeLock, чтобы не помешать другим потокам обрабатывать переменную. В случае, если вы вызвали tryConvertToWriteLock() в тот момент, когда ресурс занят для записи другим потоком, текущий поток остановлен не будет, однако метод вернёт нулевое значение. В таком случае можно вызвать writeLock() вручную.
Семафоры
Семафоры — отличный способ ограничить количество потоков, которые одновременно работают над одним и тем же ресурсом:
В этом примере сервис-исполнитель может потенциально запустить все 10 вызываемых потоков, однако мы создали семафор, который ограничивает количество одновременно выполняемых потоков до пяти. Снова напомню, что важно освобождать ресурсы именно в блоке finally<> на случай выброса исключений. Для приведённого выше кода вывод будет следующим:
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Это была вторая часть серии статей про многопоточное программирование. Настоятельно рекомендую разобрать вышеприведенные примеры самостоятельно. Все они, как обычно, доступны на GitHub. Можете смело форкать репозиторий и добавлять его в избранное.
Надеюсь, вам понравилась статья. Если у вас возникли какие-либо вопросы, вы можете задать их в твиттере.
В чем смысл использования reentrantreadwritelock
Этот class не налагает привилегированное упорядочивание читателя или писателя для доступа блокировки. Однако, это действительно поддерживает дополнительную политику справедливости. Несправедливый режим (значение по умолчанию) Когда создано как неярмарка (значение по умолчанию), порядок записи в чтение и блокировку записи является неуказанным согласно ограничениям повторной входимости. Несправедливая блокировка, с которой непрерывно спорят, может неопределенно отложить один или более потоков читателя или писателя, но будет обычно иметь более высокую пропускную способность чем справедливая блокировка.
Справедливый режим Когда создано как ярмарка, потоки борются за запись, используя приблизительно политика порядка прибытия. Когда в настоящий момент сохраненная блокировка будет выпущена, или дольше всего ожидающий единственный поток писателя будет присвоен блокировка записи, или если будет группа потоков читателя, ожидающих дольше чем все потоки писателя ожидания, то та группа будет присвоена блокировка чтения.
Поток, который пытается получить справедливую блокировку чтения (неповторно используемо), блокирует, если или блокировка записи будет сохранена, или есть поток писателя ожидания. Поток не будет получать блокировку чтения до окончания самого старого, в настоящий момент поток писателя ожидания получил и выпустил блокировку записи. Конечно, если писатель ожидания откажется от ожидать, оставляя один или более потоков читателя как самые длинные официанты в очереди со свободной блокировкой записи, то те читатели будут присвоены блокировка чтения.
Поток, который пытается получить справедливую блокировку записи (неповторно используемо), блокирует, если оба, которых блокировка чтения и пишет блокировке, не будут свободны (который подразумевает, что нет никаких потоков ожидания). (Отметьте что неблокирование ReentrantReadWriteLock.ReadLock.tryLock() и ReentrantReadWriteLock.WriteLock.tryLock() методы не соблюдают эту справедливую установку и сразу получат блокировку, если это возможно, независимо от потоков ожидания.)
Этот class поддерживает методы, чтобы определить, сохранены ли блокировки или спорили. Эти методы разрабатываются для состояния системы контроля, не для управления синхронизацией.
Сериализация этого class ведет себя таким же образом как встроенные блокировки: десериализованная блокировка находится в разблокированном состоянии, независимо от его состояния когда сериализировано.
Демонстрационные использования. Вот эскиз кода, показывающий, как выполнить блокировку, понижая после обновления кэша (обработка исключений особенно хитра, обрабатывая многократные блокировки невложенным способом): ReentrantReadWriteLocks может использоваться, чтобы улучшить параллелизм в некотором использовании некоторых видов Наборов. Это обычно стоит только, когда наборы, как ожидают, будут большими, получаются доступ большим количеством потоков читателя чем потоки писателя, и влекут за собой операции с издержками, которые перевешивают издержки синхронизации. Например, вот class, используя TreeMap, который, как ожидают, будет крупным и одновременно полученным доступ.
Примечания реализации
Эта блокировка поддерживает максимум 65535 рекурсивных блокировок записи и 65535 блокировок чтения. Попытки превысить эти пределы приводят к Error броски от блокировки методов.
Учебник по параллелизму Java — Блокировка: явные блокировки
1. Введение
Во многих случаях использование неявной блокировки достаточно. В других случаях нам понадобятся более сложные функциональные возможности. В таких случаях пакет java.util.concurrent.locks предоставляет нам объекты блокировки. Когда речь идет о синхронизации памяти, внутренний механизм этих блокировок такой же, как и при неявных блокировках. Разница в том, что явные блокировки предлагают дополнительные функции.
Основные преимущества или улучшения по сравнению с неявной синхронизацией:
2. Классификация объектов блокировки
Объекты блокировки реализуют один из следующих двух интерфейсов:
Следующая диаграмма классов показывает отношение между различными классами блокировки:
3. ReentrantLock
Эта блокировка работает так же, как синхронизированный блок; один поток получает блокировку до тех пор, пока она еще не получена другим потоком, и он не снимает ее, пока не будет активирована разблокировка. Если блокировка уже получена другим потоком, то поток, пытающийся ее получить, блокируется, пока другой поток не освободит ее.
Мы собираемся начать с простого примера без блокировки, а затем добавим блокировку повторного входа, чтобы посмотреть, как она работает.
Поскольку приведенный выше код не синхронизирован, потоки будут чередоваться. Давайте посмотрим на вывод:
Теперь мы добавим блокировку повторного входа для сериализации доступа к методу run:
Приведенный выше код будет безопасно выполняться без чередования потоков. Вы можете понять, что мы могли бы использовать синхронизированный блок, и эффект был бы таким же. Возникает вопрос: какие преимущества дает нам блокировка повторного входа?
Основные преимущества использования этого типа замка описаны ниже:
Давайте рассмотрим пример использования tryLock перед тем, как перейти к следующему классу блокировки.
3.1 Попытка получения блокировки
В следующем примере у нас есть два потока, пытающихся получить одинаковые две блокировки.
Один поток получает lock2, а затем блокирует попытки получить lock1 :
В чем смысл использования reentrantreadwritelock
This class does not impose a reader or writer preference ordering for lock access. However, it does support an optional fairness policy. Non-fair mode (default) When constructed as non-fair (the default), the order of entry to the read and write lock is unspecified, subject to reentrancy constraints. A nonfair lock that is continuously contended may indefinitely postpone one or more reader or writer threads, but will normally have higher throughput than a fair lock. Fair mode When constructed as fair, threads contend for entry using an approximately arrival-order policy. When the currently held lock is released, either the longest-waiting single writer thread will be assigned the write lock, or if there is a group of reader threads waiting longer than all waiting writer threads, that group will be assigned the read lock.
A thread that tries to acquire a fair read lock (non-reentrantly) will block if either the write lock is held, or there is a waiting writer thread. The thread will not acquire the read lock until after the oldest currently waiting writer thread has acquired and released the write lock. Of course, if a waiting writer abandons its wait, leaving one or more reader threads as the longest waiters in the queue with the write lock free, then those readers will be assigned the read lock.
A thread that tries to acquire a fair write lock (non-reentrantly) will block unless both the read lock and write lock are free (which implies there are no waiting threads). (Note that the non-blocking ReentrantReadWriteLock.ReadLock.tryLock() and ReentrantReadWriteLock.WriteLock.tryLock() methods do not honor this fair setting and will immediately acquire the lock if it is possible, regardless of waiting threads.)
This class supports methods to determine whether locks are held or contended. These methods are designed for monitoring system state, not for synchronization control.
Serialization of this class behaves in the same way as built-in locks: a deserialized lock is in the unlocked state, regardless of its state when serialized.
Sample usages. Here is a code sketch showing how to perform lock downgrading after updating a cache (exception handling is particularly tricky when handling multiple locks in a non-nested fashion): ReentrantReadWriteLocks can be used to improve concurrency in some uses of some kinds of Collections. This is typically worthwhile only when the collections are expected to be large, accessed by more reader threads than writer threads, and entail operations with overhead that outweighs synchronization overhead. For example, here is a class using a TreeMap that is expected to be large and concurrently accessed.
Implementation Notes
This lock supports a maximum of 65535 recursive write locks and 65535 read locks. Attempts to exceed these limits result in Error throws from locking methods.
В чем смысл использования reentrantreadwritelock
Acquires the write lock if neither the read nor write lock are held by another thread and returns immediately, setting the write lock hold count to one.
If the current thread already holds the write lock then the hold count is incremented by one and the method returns immediately.
If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the write lock has been acquired, at which time the write lock hold count is set to one.
lockInterruptibly
Acquires the write lock if neither the read nor write lock are held by another thread and returns immediately, setting the write lock hold count to one.
If the current thread already holds this lock then the hold count is incremented by one and the method returns immediately.
If the write lock is acquired by the current thread then the lock hold count is set to one.
In this implementation, as this method is an explicit interruption point, preference is given to responding to the interrupt over normal or reentrant acquisition of the lock.
tryLock
tryLock
If the write lock is acquired then the value true is returned and the write lock hold count is set to one.
If the specified waiting time elapses then the value false is returned. If the time is less than or equal to zero, the method will not wait at all.
In this implementation, as this method is an explicit interruption point, preference is given to responding to the interrupt over normal or reentrant acquisition of the lock, and over reporting the elapse of the waiting time.
unlock
If the current thread is the holder of this lock then the hold count is decremented. If the hold count is now zero then the lock is released. If the current thread is not the holder of this lock then IllegalMonitorStateException is thrown.