Добро пожаловать в руководство по примеру блокировки в Java. Обычно, когда работаем в многопоточной среде, мы используем synchronized для обеспечения потокобезопасности.
Java Lock
В большинстве случаев, ключевое слово synchronized является предпочтительным, но у него есть некоторые недостатки, которые привели к включению API Lock в пакет Java Concurrency. В Java 1.5 Concurrency API был добавлен пакет
java.util.concurrent.locks
с интерфейсом Lock
и некоторыми реализациями для улучшения механизма блокировки объектов. Некоторые важные интерфейсы и классы в API Lock в Java:
-
Lock: Это базовый интерфейс для API Lock. Он предоставляет все возможности ключевого слова synchronized, а также дополнительные способы создания различных условий для блокировки, установки времени ожидания для потока, ожидающего блокировки. Некоторые из важных методов: lock() для получения блокировки, unlock() для освобождения блокировки, tryLock() для ожидания блокировки в течение определенного периода времени, newCondition() для создания условия и т.д.
-
Условие: Объекты условия подобны модели Ожидание-уведомление объекта с дополнительной возможностью создания различных наборов ожидания. Объект условия всегда создается объектом блокировки. Некоторые из важных методов: await(), аналогичный wait(), и signal(), signalAll(), аналогичные методам notify() и notifyAll().
-
ReadWriteLock: Он содержит пару связанных блокировок: одну для операций только для чтения и другую для записи. Блокировка чтения может быть удерживаемой одновременно несколькими потоками-читателями, пока нет потоков-писателей. Блокировка записи является исключительной.
-
ReentrantLock: Это наиболее широко используемый класс реализации интерфейса Lock. Этот класс реализует интерфейс Lock таким же способом, как ключевое слово synchronized. Помимо реализации интерфейса Lock, ReentrantLock содержит некоторые вспомогательные методы для получения потока, удерживающего блокировку, потоков, ожидающих получения блокировки и т. д. Синхронизированные блоки являются рекурсивными по своей природе, то есть если у потока есть блокировка на объекте монитора и если другому синхронизированному блоку требуется блокировка на том же объекте монитора, то поток может войти в этот блок кода. Думаю, что это причина, по которой имя класса ReentrantLock. Давайте разберем эту особенность на простом примере.
public class Test{ public synchronized foo(){ //сделать что-то bar(); } public synchronized bar(){ //сделать еще что-то } }
Если поток входит в foo(), у него есть блокировка на объекте Test, поэтому когда он пытается выполнить метод bar(), потоку разрешено выполнить метод bar(), поскольку он уже удерживает блокировку на объекте Test, т.е. то же самое, что и synchronized(this).
Пример использования блокировки – ReentrantLock в Java
Теперь давайте рассмотрим простой пример, где мы заменим ключевое слово synchronized на Java Lock API. Допустим, у нас есть класс Resource с некоторыми операциями, где мы хотим, чтобы он был потокобезопасным, и некоторыми методами, где потокобезопасность не требуется.
package com.journaldev.threads.lock;
public class Resource {
public void doSomething(){
//выполняем какие-то операции, чтение из БД, запись и т. д.
}
public void doLogging(){
//логирование, без необходимости в потокобезопасности
}
}
Теперь давайте предположим, у нас есть класс Runnable, где мы будем использовать методы ресурса.
package com.journaldev.threads.lock;
public class SynchronizedLockExample implements Runnable{
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
Обратите внимание, что я использую синхронизированный блок для захвата блокировки объекта ресурса. Мы могли бы создать фиктивный объект в классе и использовать его для целей блокировки. Теперь давайте посмотрим, как мы можем использовать API блокировки в Java и переписать вышеуказанную программу, не используя ключевое слово synchronized. Мы будем использовать ReentrantLock в Java.
package com.journaldev.threads.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//освобождение блокировки
lock.unlock();
}
resource.doLogging();
}
}
Как видите, я использую метод tryLock(), чтобы убедиться, что мой поток ждет только определенное время, и если не получает блокировку объекта, то просто регистрирует и завершается. Еще одно важное замечание – использование блока try-finally, чтобы убедиться, что блокировка освобождается даже если вызов метода doSomething() выбрасывает исключение.
Java Lock vs synchronized
Исходя из вышеуказанных деталей и программы, мы можем легко сделать следующие выводы о различиях между Java Lock и синхронизацией.
- API блокировки Java предоставляет больше видимости и вариантов для блокировки, в отличие от synchronized, где поток может оказаться в ожидании бесконечно долго, мы можем использовать tryLock() чтобы убедиться, что поток ждет только определенное время.
- Код синхронизации намного чище и легче поддерживать, в то время как с Lock нам приходится использовать блок try-finally, чтобы убедиться, что блокировка освобождается даже если между вызовами методов lock() и unlock() выбрасывается какое-либо исключение.
- синхронизационные блоки или методы могут охватывать только один метод, в то время как мы можем захватить блокировку в одном методе и освободить ее в другом методе с использованием API блокировки.
- Ключевое слово synchronized не обеспечивает справедливость, в то время как мы можем установить справедливость в true при создании объекта ReentrantLock, чтобы самый долго ожидающий поток первым получил блокировку.
- Мы можем создавать различные условия для блокировки, и разные потоки могут ожидать() различных условий.
Это все для примера использования блокировки в Java, ReentrantLock в Java и сравнительного анализа с ключевым словом synchronized.
Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock