Краткий обзор оптимистических и пессимистических блокировок.
В этой заметке рассматриваются основные различия этих двух подходов на примере реализации целочисленного счетчика на Java
.
Статья из моего telegram канала: Senior’s Blog. Подписывайтесь на канал ;-)
Блокировки
Блокировки в многопоточной среде необходимы для одновременной работы двух и более потоков с одними и теми же данными. Блокировки позволяют избежать потери и повреждения данных. Существуют два подхода к блокировкам: оптимистические и пессимистические.
Суть пессимистических блокировок в эксклюзивном доступе к данным, т.е. когда один поток получил пессимистическую блокировку на данные, другие потоки не могут читать и изменять эти данные, пока поток не снимет блокировку. Пессимистические блокировки достаточно простые в реализации, но обладают очень важным недостатком - дедлоками (deadlock).
Оптимистические блокировки устроены по другому принципу. Они существуют во многих вариациях, я расскажу про самую простую - CAS. Когда два потока хотят изменить одни и те же данные, потоки копируют эти данные в свою локальную память, затем меняют их и пытаются отправить измененные данные в основную память. Перед внесением изменений в основную память проверяется версия данных или предыдущее значение данных. Если проверка не проходит, то поток снова копирует к себе уже новые данные, вносит в них изменения и снова пытается отправить эти измененные данные в основную память.
Оптимистические блокировки иногда называют не блокируемыми (non-blocking), потому что при изменении данных поток работает со своей локальной копией данных, а данные в основном хранилище остаются открытыми другим потоках для чтения и изменения.
Простой пример оптимистических блокировок с которыми каждый день сталкивается любой разработчик - это системы контроля версий, например Git. Вы копируете себе ветку с кодом, модифицируете ее и пытаетесь слить с мастером, если кто-то обновил мастер до вас, вам надо скопировать себе его изменения, исправить конфликты и снова попытаться слить свою ветку с мастером.
В этой статье сравниваются эти два вида блокировок на примере баз данных.
Java
Рассмотрим реализацию оптимистических и пессимистических блокировок в Java
.
В качестве примера реализуем целочисленный счетчик.
Для пессимистических блокировок наиболее простым решением будет использование блока synchronized
.
Получаем простенький класс:
public class Counter {
private int counter;
public int incrementAndGet() {
synchronized (this) {
counter += 1;
return counter;
}
}
public int get() {
synchronized (this) {
return counter;
}
}
}
Не забываем, что synchronize
нужен не только при записи но и при чтении счетчика.
И в этом случае counter
необязательно объявлять как volatile
,
потому что synchronized
гарантирует синхронизацию данных в оперативной памяти и в памяти потока при входе в блок.
Если интересна эта тема почитайте про Happens Before Guarantee
, например тут.
Для оптимистических блокировок все уже сделано за нас - это AtomicInteger
.
Если посмотреть на код AtomicInteger
, то там обнаружится обычная volatile
переменная
private volatile int value;
И функция которая обновляет эту переменную с помощью Compare-and-swap (CAS)
алгоритма:
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
Эта функция будет использовать нативную инструкцию процессора, которая атомарно выполнит сравнение и обновление значения переменной.
Подробнее про реализацию CAS в JVM можно почитать тут.