Обычно в многопоточной среде для достижения потокобезопасности, мы используем ключевое слово 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().»