Оптимистические и Пессимистические Блокировки

Краткий обзор оптимистических и пессимистических блокировок. В этой заметке рассматриваются основные различия этих двух подходов на примере реализации целочисленного счетчика на 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 можно почитать тут.