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


Статья про оптимистические и пессимистические блокировки.

Статья из моего telegram канала: Senior’s Blog. Подписывайтесь на канал ;-)

Блокировки

Сегодня поговорим про оптимистические и пессимистические блокировки.

Блокировки в многопоточной среде необходимы для одновременной работы двух и более потоков с одними и теми же данными. Блокировки позволяют избежать потери и повреждения данных. Существуют два подхода к блокировкам: оптимистические и пессимистические.

Суть пессимистических блокировок в эксклюзивном доступе к данным, т.е. когда один поток получил пессимистическую блокировку на данные, другие потоки не могут прочитать или или изменить эти данные, пока поток не снимет блокировку. Пессимистические блокировки достаточно простые в реализации, но обладают очень важным недостатком - дедлоками (deadlock).

Оптимистические блокировки устроены по другому принципу. Они существуют во многих вариациях, я расскажу про самую простую. Когда два потока хотят изменить одни и те же данные, они копируют эти данные в свою локальную память, меняют их и потом пытаются отправить изменения в основную память. Перед внесением изменений в основную память проверяется версия данных или предыдущие значения данных (CAS), если они не совпадают, то поток снова копирует к себе уже новые данные, вносит в них изменения и снова пытается отправить эти изменения в основную память.

Оптимистические блокировки еще можно назвать не блокируемыми (non-blocking), потому что при изменении данных мы работаем с локальной копией данных в треде, а данные в основном хранилище остаются открытыми для чтения и изменения для других тредов.

Простой пример оптимистических блокировок которые мы каждый день используем это системы контроля версий, например Git. Вы копируете себе ветку с кодом, модифицируете ее и пытаетесь слить с мастером, если кто то обновил мастер до вас, вам надо скопировать себе его изменения, исправить конфликты и снова попытаться слить свою ветку с мастером.

В этой статье сравниваются эти два вида блокировок на примере баз данных.

Один из самых известных алгоритмов оптимистических блокировок - Compare-and-swap (CAS).

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 то мы увидим обычный

private volatile int value;

который обновляется с помощью Compare-and-swap (CAS) алгоритма:

public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);

Эта функция будет использовать нативную инструкцию процессора, которая атомарно выполнит сравнение и обновление значения.

Подробнее про реализацию CAS в JVM можно почитать тут.