Блокировки
Одной из популярных функциональных возможностей библиотек J2SE 5.0 является добавление средств обеспечения параллельной работы. Предоставленные как часть JSR 166 эти средства обеспечивают развитые возможности программирования параллельных процессов, устраняющие необходимость использования разработчиками ключевого слова synchronized
и связанных с ним блокировок. Среди предлагаемых ими функциональных возможностей присутствуют: поддержка блокировочных таймаутов, множественные переменные условия для одной блокировки, блокировки чтения/записи и способность прерывать поток, ожидающий снятия блокировки. Более подробную информацию по дополнительной поддержке блокировок можно найти в документации по пакету
java.util.concurrent.locks
.
Основой пакета является интерфейс Lock
, который предлагает набор методов для запроса и снятия блокировки. Типичной последовательностью использования является: получить блокировку, обратиться к защищенному ресурсу, снять блокировку. Вот схема такого использования:
Lock l = ...;
l.lock();
try {
// доступ к защищенному ресурсу
} finally {
l.unlock();
}
Когда класс Lock
используется таким способом, его работа похожа на работу обычной блокировки synchronized
:
synchronized(lockVariable) {
// доступ к защищенному ресурсу
}
Похожа, но не идентична. Если вы не вызовете метод unlock()
в блоке finally
, ваше приложение не будет работать правильно. В блоке synchronized
это не обязательно.
Еще одним методом, предоставляемым интерфейсом Lock
и запрашивающим блокировку, является lockInterruptibly()
. Поток после запроса блокировки переходит в состояние ожидания. Метод lockInterruptibly()
дает вам возможность прервать ожидающий поток, а метод lock()
- нет. Если в ожидающем блокировки с lockInterruptibly()
потоке вызывается метод interrupt()
, ожидание будет прервано. Ожидающий поток просыпается, и генерируется исключительная ситуация InterruptedException
. В этом случае попыток доступа к защищенному ресурсу не делается, а продолжаются любые другие действия.
Обычная схема использования метода lockInterruptibly()
:
Lock l = new ReentrantLock();
try {
l.lockInterruptibly();
try {
// доступ к защищенному ресурсу
} finally {
l.unlock();
}
} catch (InterruptedException e) {
System.err.println("Interrupted wait");
}
Здесь внутренний блок try-finally
обрабатывает исключительные ситуации, возможные при работе с защищенным ресурсом. Внешний блок try-catch
обрабатывает исключительные ситуации при запросе ресурса. Вы никогда не должны вызывать unlock
, если запрос блокировки терпит неудачу.
Предупреждение: вызов lockInterruptibly()
не обязательно означает, что вы сможете прервать ожидание. Не все реализации Lock
поддерживают эту операцию.
Еще один метод, предоставляемый интерфейсом Lock
, tryLock()
, вводит в игру таймауты. Метод tryLock()
имеет две версии. Первая версия не принимает аргументов. Она делает попытку получить блокировку и немедленно терпит неудачу при недоступности этой блокировки:
Lock lock = ...;
if (lock.tryLock()) {
try {
// доступ к защищенному ресурсу
} finally {
lock.unlock();
}
} else {
// альтернативное действие
}
Это похоже, например, на то, что вы, как будто бы, подошли к копировальному аппарату, увидели, что он занят ддительной по времени работой, и перешли работать на более медленный, но свободный аппарат.
Вторая версия метода tryLock
принимает два параметра для указания таймаута. Первый аргумент – это переменная long
, указывающая максимальное время ожидания. Второй аргумент – это TimeUnit
, указывающий единицу времени.
Например, lock.tryLock(300, TimeUnit.MILLISECONDS)
делает попытку получить блокировку. Если блокировку получить нельзя после истечения 300 миллисекунд, возвращается значение false
, обозначающее невозможность получения блокировки. Для сравнения, lock.tryLock(30, TimeUnit.SECONDS)
ожидает максимум 30 секунд. Вы должны явно указывать оба аргумента.
Класс TimeUnit
позволяет указывать единицу времени в секундах, миллисекундах, микросекундах или наносекундах.
1 секунда = 1 тысяча миллисекунд
1 секунда = 1 миллион микросекунд
1 секунда = 1 миллиард наносекунд
Вы уже увидели, как использовать интерфейс Lock
, но вы еще не видели, как фактически создать Lock
. Во всех предыдущих примерах использовалось:
Lock lock = ...;
Пакет java.util.concurrent.locks
включает три реализации интерфейса Lock
:
-
ReentrantLock
-
ReentrantReadWriteLock.ReadLock
-
ReentrantReadWriteLock.WriteLock
Обычно используется первая реализация, ReentrantLock
. Другие две реализации являются внутренними классами класса ReentrantReadWriteLock
. Класс ReentrantReadWriteLock
содержит две блокировки: ReadLock
и WriteLock
.
Типовая схема использования ReentrantLock
:
Lock l = new ReentrantLock();
l.lock();
try {
// доступ к защищенному ресурсу
} finally {
l.unlock();
}
Используйте блокировку чтение-запись при длительных и частых операциях чтения и нечастых операциях записи. Тогда при доступе к защищенному ресурсу вы должны будете использовать разные блокировки, как показано ниже:
ReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock();
Lock writeLock = rwl.writeLock();
Далее вы используете блокировки таким же способом, как и ReentractLock
, применяя тип блокировки, соответствующий желаемому типу доступа.
Единственной реализацией интерфейса ReadWriteLock
является сам класс theReentrantReadWriteLock
.
Дополнительную информацию по поддержке блокировок можно найти в описании пакета.