StampedLock в Java
В Java 8 в пакете java.util.concurrent.locks появился интересный класс – StampedLock. Этот класс в ряде случаев приносит исключительную пользу, однако не все даже опытные программисты про него знают. Сегодня мы немного подправим эту досадную ситуацию.
Из названия очевидно, что класс StampedLock реализует механизм блокировок и является функциональным аналогом хорошо известным механизмам synchronized и ReentrantLock.
Оптимистичная блокировка
У StampedLock есть ряд интересных особенностей, сегодня мы рассмотрим одну из них – «оптимистичная блокировка». «Оптимистичная блокировка» – широко известный принцип в организации многопользовательского доступа к базам данных. Принцип работы очень простой – читаем данные, надеясь, что их никто не успел изменить. Если всё же кто-то поменял, то читаем ещё раз или выставляем блокировку (если уровень оптимизма уменьшился и читаем ещё раз.
Рассмотрим пример
Есть общая переменная. Один поток эту переменную меняет, два другие читают. Причём поток-писатель делает своё дело долго, но относительно редко. А читатели читают часто, но быстро. Как бы мы реализовали эту схему «традиционными средствами»? Писатель и читатели блокировали бы общую переменную для выполнения своих действий. При этом они мешали бы друг другу и общая производительность системы снижалась бы. Мы могли бы использовать раздельные блокировки – на чтение и на запись. Стало бы лучше, но не сильно. Потоки всё равно «мешали» бы друг другу. Т. к. писатель один и работает редко, мы можем использовать «оптимистичную блокировку» в надежде на то, что писатель в большинстве случаев не успевает изменить данные.
Как это выглядит в коде:
private void counterWriter() { try { while (!Thread.currentThread().isInterrupted()) { long stamp = sl.writeLock(); // выставляем блокировку на запись try { long tmp = counter; System.out.println("start counter modification:" + tmp); Thread.sleep(10_000); tmp++; counter = tmp; // изменяем общую переменную. System.out.println("end counter modification:" + tmp); } finally { sl.unlockWrite(stamp); //снимаем блокировку на запись } Thread.sleep(30_000); } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } }
Как выглядит читатель:
private void counterReader(int id) { try { while (!Thread.currentThread().isInterrupted()) { long stamp = sl.tryOptimisticRead(); // берем метку состояния long tmp = counter; // читаем значение общей переменной if (!sl.validate(stamp)) { // проверяем метку состояния, System.out.println(" id:" + id + " protected value has been changed"); stamp = sl.readLock(); // если состояние изменилось, ставим блокировку System.out.println(" id:" + id + " new readLock"); try { tmp = counter; // читаем данные под блокировкой } finally { sl.unlockRead(stamp); // снимаем блокировку } } System.out.println(" id:" + id + " current value:" + tmp); Thread.sleep(1_000); } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } }
Логика работы основана на метке – значении, которое отражает состояние данных, которые мы защищаем критической секцией.
Если между моментом «фиксации состояния» (long stamp = sl.tryOptimisticRead();) и проверкой (sl.validate(stamp))) метка изменилась, значит кто-то изменил общее состояние, и данные надо перечитать.
Почему этот подход более эффективен, чем например synchronized? Т. к. данные меняются редко, то нет необходимости на каждое чтение выставлять блокировку, которая является весьма ресурсозатратной операцией.
Полный пример находится по ссылке.