Обычно в многопоточной среде для достижения потокобезопасности, мы используем ключевое слово synchronized. Однако сегодня мы рассмотрим конкурента этому способу в виде Lock API.
В большинстве случаев, ключевое слово synchronized является хорошим выбором, но все же имеет некоторые недостатки. Именно поэтому еще в Java 1.5 был введен Concurrency API и пакет java.util.concurrent.locks c интерфейсом Lock и некоторыми дополнительными классами, которые усовершенствовали механизм блокировки.
Важные моменты в Concurrency Lock API
- Lock: Это базовый интерфейс в
Lock API. Он обеспечивает все функции ключевого словаsynchronized, добавляя новые методы для удобной работы. Например:
- метод
lock()используется для того, чтобы получить lock для работы; - метод
unlock()— освободить lock; - метод
tryLock()для ожидания лока на протяжении определенного времени; - метод
newCondition()— создатьConditionи т.п.
Condition: Это похоже наwait-notifyмодель с рядом дополнительных функций. ОбъектConditionвсегда создается с помощью объектаLock. Такой важный метод, какawait()очень похож наwait(), а методыsignal(),signalAll()похожи наnotify()иnotifyAll().ReadWriteLockсодержит пару связанных локов: первый только для чтения, второй для записи. Лок для чтения может предоставлять доступ одновременно для нескольких потоков.- Класс
ReentrantLock— это наиболее используемая реализация интерфейсаLock. Эта реализация интерфейсаLockаналогична использованию ключевого словаsynchronized. Кроме реализации интерфейсаLock,ReentrantLockсодержит ряд вспомогательных методов для работы с потоками.
Давайте рассмотрим использование Java Lock API на примере небольшой программы:
Допустим, у нас есть тестовый класс с синхронизированными методами обработки чего-либо.
|
1 2 3 4 5 6 7 8 9 10 11 |
public class Test{ public synchronized foo(){ // что-то делаем с этим методом bar(); } public synchronized bar(){ // метод для обработки чего-то } } |
Если поток входит в метод foo(), то происходит лок на объекте Test. Когда поток пытается выполнить метод bar(), то беспрепятственно его выполняет, потому что уже лочит объект Test. Это в точности похоже на использование метода synchronized(this).
А теперь давайте посмотрим простой пример, где можно и нужно заменить использование ключевого слова synchronized на Lock API.
И так, пусть у нас есть класс Resource с парочкой потокобезопасных методов и методов, где потокобезопасность не требуется.
|
1 2 3 4 5 6 7 8 9 10 |
public class Resource { public void doSomething(){ // пусть здесь происходит работа с базой данных } public void doLogging(){ // потокобезопасность для логгирования нам не требуется } } |
А теперь берем класс, который реализует интерфейс Runnable и использует методы класса Resource.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class SynchronizedLockExample implements Runnable{ // экземпляр класса Resource для работы с методами private Resource resource; public SynchronizedLockExample(Resource r){ this.resource = r; } @Override public void run() { synchronized (resource) { resource.doSomething(); } resource.doLogging(); } } |
Обратите внимание, что мы используем блок synchronized для доступа чтобы получить лок на объекте Resource.
А теперь давайте перепишем приведенную выше программу с использованием Lock API вместо ключевого слова synchronized.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
package ua.com.prologistic; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; // класс для работы с Lock API. Переписан с приведенной выше программы, // но уже без использования ключевого слова synchronized 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 { // пытаемся взять лок в течении 10 секунд if(lock.tryLock(10, TimeUnit.SECONDS)){ resource.doSomething(); } } catch (InterruptedException e) { e.printStackTrace(); }finally{ //убираем лок lock.unlock(); } // Для логгирования не требуется потокобезопасность resource.doLogging(); } } |
Как видно из программы, мы используем метод tryLock(), чтобы убедиться в том, что поток ждет только определенное время. Если он не получает блокировку на объект, то просто логгирует и выходит.
Еще один важный момент. Мы используем блок try-finally, чтобы убедиться в том, что блокировка будет снята, даже если метод doSomething() бросит исключение.
Преимущества и недостатки каждого из способов или Lock vs synchronized
На основании вышеизложенной информации и простого примера использования Lock API и блока synchronized, мы можем сделать следующие выводы о преимуществах и недостатках каждого из способов или же просто указать на разницу между ними.
Lock APIобеспечивает больше возможностей для блокировки, в отличие отsynchronized, где поток может бесконечно ожидать лок. ВLock APIмы можем использовать методtryLock(), чтобы ожидать лок только определенное время.- Синхронизированный код намного чище и проще в поддержке. В случае использования
Lock APIмы вынуждены писатьtry-finallyблок, чтобы убедиться в том, что блокировка будет снята, даже если между вызовами методаlock()иunlock(). - Блоки синхронизации могут покрывать только один метод, в то время как
Lock APIпозволяет получить лок в одном методе, а снять его в другом.
Вот и все, что нужно знать о Lock API, его преимуществах и недостатках перед блоком synchronized, чтобы писать простые потокобезопасные программы на Java. Подробнее о многопоточности и параллелизму читайте в отдельном разделе сайта.




Большое спасибо за внятное объяснение, для меня, как новичка, это очень важно. Возник следующий вопрос: если в классе SynchronizedLockExample объявит переменную Object lock = new Object() и переписать run(), как-то так
public void run() {
synchronized (lock) {
resource.doSomething();
}
resource.doLogging();
}
То получится, ведь, то же, что и с использованием concurrent, или нет?
«// лочим на 10 секунд
if(lock.tryLock(10, TimeUnit.SECONDS)){»
Афтор, что ты несешь. Пытаемся взять лок в течении 10 секунд, а лочим мы на столько на сколько нужно.
Да, спасибо за замечание — исправил
Не закончена мысль в
«…блокировка будет снята, даже если между вызовами метода lock() и unlock().»