Главная страница
Навигация по странице:

  • 13.1 Интерфейсы Lock и ReentrantLock

  • 13.1.1 Опрашиваемый и ограниченный по времени захват блокировки

  • Листинг 13.4

  • Листинг 13.5

  • 13.2 Вопросы производительности

  • 13.4 Выбор между synchronized и ReentrantLock

  • 13.5 Блокировки на чтение-запись

  • Преимущества при освобождении.

  • Реентерабельность.

  • Рисунок 13.3

  • При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница26 из 34
    1   ...   22   23   24   25   26   27   28   29   ...   34
    Глава 13 Явные блокировки
    До Java 5.0 единственными механизмами координации доступа к совместно используемым данным были synchronized и volatile
    . В Java 5.0 была добавлена другая опция: класс
    ReentrantLock
    . Вопреки тому, что некоторые писали, класс
    ReentrantLock не является заменой внутренней блокировки, а скорее альтернативой с расширенными функциями, когда внутренняя блокировка оказывается слишком ограниченной.
    13.1 Интерфейсы Lock и ReentrantLock
    Интерфейс
    Lock
    , приведённый в листинге 13.1, определяет несколько абстрактных блокирующих операций. Отличаясь от внутренней блокировки, интерфейс
    Lock предлагает возможность выбора из безусловного, опрашиваемого, ограниченного по времени и прерываемого захвата блокировки, и все операции блокировки и разблокировки выполняются явно. Реализации интерфейса
    Lock должны предоставлять ту же семантику видимости памяти, что и внутренние блокировки, но могут различаться в семантике блокировки, алгоритмах планирования, гарантиях упорядочивания, и характеристиках производительности. (Метод
    Lock.newCondition рассматривается в главе
    14
    ). public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException; void unlock();
    Condition newCondition();
    }
    Листинг 13.1 Интерфейс
    Lock
    Класс
    ReentrantLock реализует интерфейс
    Lock
    , обеспечивая те же гарантии взаимного исключения и видимости памяти, что и блок synchronized
    . Захват блокировки с помощью класса
    ReentrantLock имеет ту же семантику памяти, что и вход в блок synchronized
    , а освобождение блокировки, захваченной с помощью класса
    ReentrantLock
    , имеет ту же семантику памяти, что и выход из блока synchronized
    . (Видимость памяти рассматривается в разделе
    3.1
    и в Главе
    16
    .) И, как и блок synchronized
    , класс
    ReentrantLock предлагает реентерабельную
    137
    семантику блокировки (см. раздел
    2.3.2
    ). Класс
    ReentrantLock поддерживает все режимы блокировки, определенные в интерфейсе
    Lock
    , при этом обеспечивая большую гибкость при работе с недоступностью блокировки, чем блок synchronized
    Зачем создавать новый механизм блокировки, который так похож на внутреннюю блокировку? Внутренняя блокировка отлично работает в большинстве ситуаций, но имеет некоторые функциональные ограничения - невозможно прервать поток, ожидающий захвата блокировки, или попытаться захватить блокировку, не желая ожидать ее вечно. Внутренние блокировки также должны
    137
    Реентерабельность – повторная входимость.
    освобождаться в том же блоке кода, в котором они были захвачены; это упрощает кодирование и обеспечивает прекрасное взаимодействие с обработкой исключений, но делает невозможными блокировки с не-блочной структурой. Нет причин отказываться от использования блока synchronized
    , но в некоторых случаях более гибкий механизм блокировки обеспечивает лучшую живучесть или производительность.
    В листинге 13.2 приведена каноническая форма использования блокировки. Эта идиома несколько сложнее, чем использование встроенных блокировок: блокировка должна быть освобождена в блоке finally
    . В противном случае блокировка может быть никогда не освобождена, если защищаемый код бросит исключение. При использовании блокировки необходимо также учитывать, что происходит, если исключение бросается из блока try
    ; если существует вероятность того, что объект может остаться в несогласованном состоянии, может потребоваться использование дополнительных блоков try-catch или try- finally
    . (Всегда следует учитывать влияние исключений при использовании любой формы блокировки, включая внутреннюю блокировку.)
    Lock lock = new ReentrantLock(); lock.lock(); try {
    // update object state
    // catch exceptions and restore invariants if necessary
    } finally {
    lock.unlock();
    }
    Листинг 13.2 Защита состояния объекта с использованием класса
    ReentrantLock
    Пренебрежение использованием блока finally
    , в целях освобождения экземпляра
    Lock
    , представляет собой бомбу с часовым механизмом. Когда блокировка станет не нужна, вам будет трудно отследить её происхождение, поскольку не будет никаких записей о том, где и когда экземпляр
    Lock должен был быть освобожден. Это одна из причин, из-за которой не следует использовать класс
    ReentrantLock в качестве полной замены блока synchronized
    : это более “опасно”, потому что блокировка автоматически не освобождается, когда управление покидает защищаемый блок. До тех пор, пока вы помните о том, что необходимо освобождать блокировку в блоке finally
    , всё не так сложно, но всё равно существует вероятность того, что вы забудете это сделать.
    138
    13.1.1 Опрашиваемый и ограниченный по времени захват
    блокировки
    Опрашиваемые и ограниченные по времени режимы захвата блокировок, предоставляемые методом tryLock
    , позволяют выполнять более сложное восстановление после возникновения ошибок, чем при безусловном захвате. При использовании внутренних блокировок, взаимоблокировка неустранима - единственный способ восстановления заключается в перезапуске приложения, а единственная защита - создавать программу таким образом, чтобы был невозможен
    138
    Инструмент FindBugs имеет детектор "неосвобождённой блокировки", определяющий случай, когда блокировка не освобождается во всех ветках кода вне блока, в котором она была захвачена.
    несогласованный порядок захвата блокировок. Ограниченные по времени и опрашиваемые блокировки могут предложить другой вариант: вероятностное предотвращение взаимоблокировки.
    Использование ограниченных по времени или опрашиваемых блокировок
    (
    tryLock
    ) позволяет восстановить управление в том случае, если не удаётся захватить все необходимые блокировки, освободить те, которые были захвачены, и повторить попытку (или, по крайней мере, логировать информацию о сбое и выполнить что-то еще). В листинге 13.3 показан альтернативный способ устранения динамической взаимоблокировки, вызванной порядком захвата блокировок, из раздела
    10.1.2
    : используйте метод tryLock для попытки захвата обеих блокировок, выполните откат и повторите попытку, если обе блокировки одновременно захватить невозможно. Время сна имеет фиксированный и случайный компоненты, для уменьшения вероятности возникновения динамической взаимоблокировки. Если блокировки не удаётся получить в течение указанного времени, метод transferMoney возвращает статус, указывающий на то, что произошёл сбой, чтобы операция могла завершиться с ошибкой. (См. [CPJ
    2.5.1.2] и [CPJ 2.5.1.3] для дополнительных примеров использования опрашиваемых блокировок, для избежания взаимоблокировок.) public boolean transferMoney(Account fromAcct,
    Account toAcct,
    DollarAmount amount, long timeout,
    TimeUnit unit) throws InsufficientFundsException, InterruptedException { long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAcct.lock.tryLock()) { try { if (toAcct.lock.tryLock()) { try { if (fromAcct.getBalance().compareTo(amount)
    < 0) throw new InsufficientFundsException(); else { fromAcct.debit(amount); toAcct.credit(amount); return true;
    }
    } finally { toAcct.lock.unlock();
    }
    }
    } finally { fromAcct.lock.unlock();
    }
    }
    if (System.nanoTime() > stopTime) return false;
    NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
    }
    }
    Листинг 13.3 Избегание возникновения взаимоблокировок с помощью метода tryLock
    Блокировки, ограниченные по времени, также полезны при реализации активностей, управляющих бюджетом времени (см. раздел
    6.3.7
    ). Когда действие с определённым бюджетом времени вызывает блокирующий метод, оно может предоставить время ожидания, соответствующее оставшемуся времени в бюджете.
    Это позволяет завершать действия раньше, если они не смогут предоставить результат в течение указанного промежутка времени. При использовании внутренних блокировок, отменить захват блокировки после запуска операции уже невозможно, поэтому внутренние блокировки ставят под угрозу возможность реализации действий, запланированных по времени.
    В примере портала путешествий, приведённом в листинге 6.17 (в разделе
    6.3.8
    ), создаётся отдельная задача для каждой компании по прокату автомобилей, от которой запрашивались предложения. Запрос ставки, вероятнее всего, включает в себя какой-то механизм запроса по сети, например, запрос к веб-службе. Однако, для получения предложения может также потребоваться эксклюзивный доступ к дефицитному ресурсу, подобному прямой линии связи с компанией.
    Ранее, в разделе
    9.5
    , мы видели один способов обеспечить сериализованный доступ к ресурсу: однопоточный исполнитель. Другой подход заключается в использовании эксклюзивной блокировки для ограничения доступа к ресурсу. Код, приведенный в листинге 13.4, пытается отправить сообщение по совместно используемой линии связи, защищённой экземпляром
    Lock
    , но корректно завершает работу, если не может выполнить это в рамках своего бюджета времени.
    Ограниченная по времени версия метода tryLock практически включает эксклюзивную блокировку в такую ограниченную по времени активность. public boolean trySendOnSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException { long nanosToLock = unit.toNanos(timeout)
    - estimatedNanosToSend(message); if (!lock.tryLock(nanosToLock, NANOSECONDS)) return false; try { return sendOnSharedLine(message);
    } finally { lock.unlock();
    }
    }
    Листинг 13.4 Блокировка с ограниченным бюджетом времени
    13.1.2 Прерываемый захват блокировки
    Также как ограниченный по времени захват блокировки позволяет эксклюзивным блокировкам использоваться в активностях, ограниченных по времени, также и прерываемый захват блокировки позволяет блокировкам использоваться в
    отменяемых активностях. В разделе
    7.1.6
    было определено несколько механизмов, подобных захвату внутренней блокировки, не реагирующей на прерывание. Эти не прерываемые механизмы блокировки усложняют реализацию отменяемых задач.
    Метод lockInterruptibly позволяет вам попытаться захватить блокировку, оставаясь отзывчивым к прерыванию, и его включение в интерфейс
    Lock позволяет избежать создания другой категории не прерываемых механизмов блокировки.
    Каноническая структура прерываемого захвата блокировки немного сложнее, чем обычный захват блокировки, поскольку необходимы два блока try
    . (Если прерываемый захват блокировки может бросить исключение InterruptedException, работает стандартная идиома захвата блокировки try-finally
    ). В листинге 13.5 метод lockInterruptibly используется для реализации метода sendOnSharedLine из листинга 13.4, так что мы можем вызвать его из задачи, которую можно отменить. Ограниченный по времени метод tryLock также отзывчив к прерыванию, и поэтому может быть использован, когда вам необходим и ограниченный по времени, и прерываемый захват блокировки. public boolean sendOnSharedLine(String message) throws InterruptedException { lock.lockInterruptibly(); try { return cancellableSendOnSharedLine(message);
    } finally { lock.unlock();
    }
    } private boolean cancellableSendOnSharedLine(String message) throws InterruptedException { ... }
    Листинг 13.5 Прерываемый захват блокировки
    13.1.3 Блокировка с не-блочной структурой
    В случае с внутренними блокировками, пары захват-освобождение имеют блочную структуру - блокировка всегда освобождается в том же базовом блоке, в котором она была захвачена, независимо от того, каким образом управление покинет блок.
    Автоматическое освобождение блокировки упрощает анализ и предотвращает возможные ошибки кодирования, но иногда требуется более гибкая дисциплина блокировки.
    Ранее, в главе
    11
    , мы видели, как уменьшение детализации блокировок может повысить масштабируемость. Чередование блокировок позволяет различным цепочкам хэшей, в коллекции на основе хэшей, использовать разные блокировки.
    Мы можем применить аналогичный принцип, чтобы уменьшить степень детализации блокировки в связанном списке, используя отдельную блокировку для
    каждой ссылки на узел, позволяя различным потокам работать независимо в разных частях списка. Блокировка для данного узла защищает указатели ссылок и данные, хранящиеся в этом узле, поэтому при обходе или изменении списка мы должны удерживать блокировку на одном узле, пока не получим блокировку на следующем узле; только после этого можно отпустить блокировку на первом узле.
    Пример такого подхода, называемый блокировкой рука об руку (hand-over-hand) или связью блокировок (lock coupling), приведен в [CPJ 2.5.1.4].

    13.2 Вопросы производительности
    Когда класс
    ReentrantLock был добавлен в Java 5.0, он предлагал гораздо лучшую производительность, чем внутренние блокировки. Для примитивов синхронизации, производительность при конкуренции является ключом к масштабируемости: если на управление блокировками и планирование расходуется больше ресурсов, для приложения, соответственно, доступно меньше. Лучшая реализация блокировки осуществляет меньше системных вызовов, вынуждает выполнять меньше переключений контекста и инициирует меньше трафика синхронизации доступа к памяти в совместно используемой шине доступа к памяти - перечень операций, которые требуют много времени на выполнение и отвлекают вычислительные ресурсы программы.
    В Java 6 используется улучшенный алгоритм управления внутренними блокировками, подобный тому, который используется в классе
    ReentrantLock
    , что приводит к значительному сокращению разрыва в масштабируемости. На рис. 13.1 показана разница в производительности между встроенными блокировками и классом
    ReentrantLock в Java 5.0 и в предварительной сборке Java 6, на четырех- ядерной системе Opteron запущенной на ОС Solaris. Кривые демонстрируют
    “ускорение” класса
    ReentrantLock по сравнению с внутренней блокировкой на одной и той же версией JVM. На кривой Java 5.0 класс
    ReentrantLock предлагает значительно лучшую пропускную способность, но на кривой Java 6 оба значения довольно близки
    139
    . Тестовая программа та же, что использовалась в разделе 11.5, но на этот раз сравнивается пропускная способность
    HashMap
    , защищенная встроенной блокировкой и классом
    ReentrantLock
    Рисунок 13.1 Сравнение производительности внутренней блокировки и класса
    ReentrantLock
    , в Java 5 и Java 6
    В Java 5.0 производительность внутренней блокировки резко падает при переходе от выполнения одного потока (без конкуренции) к выполнению нескольких потоков; производительность класса
    ReentrantLock страдает намного меньше, демонстрируя его лучшую масштабируемость. Но в Java 6 совсем другая история - внутренние блокировки больше не разваливаются из-за конкуренции, и оба масштаба довольно похожи.
    Графики подобные тому, что приводится на рисунке 13.1, напоминают нам, что утверждения вида “X быстрее Y” в лучшем случае недолговечны.
    139
    Хотя график этого и не отражает, разница в масштабируемости между Java 5.0 и Java 6 действительно связана с улучшением внутренней блокировки, а не с регрессией производительности в классе
    ReentrantLock

    Производительность и масштабируемость зависят от таких факторов, определяемых платформой, как ЦП, количество процессоров, размер кэша и характеристики JVM, которые со временем могут изменяться
    140
    Производительность представляет собой изменяющийся показатель; вчерашний бенчмарк, показывающий, что X быстрее, чем Y, возможно сегодня уже устарел.
    13.3 Справедливость
    Конструктор класса
    ReentrantLock предлагает выбор из двух вариантов справедливости: создать несправедливую (nonfair) блокировку (по умолчанию) или справедливую (fair) блокировку. Потоки захватывают справедливую блокировку в том порядке, в котором они ее запросили, в то время как несправедливая блокировка разрешает баржирование (barging)
    141
    : потоки, запрашивающие блокировку, могут опережать очередь ожидающих потоков, если блокировка оказывается доступной при запросе. (Класс
    Semaphore также предлагает выбор из честного и нечестного порядка захвата блокировок.) “Несправедливые” экземпляры класса
    ReentrantLock не стараются изо всех сил способствовать баржированию - они просто не препятствуют потоку в баржировании, если он появляется в нужное время. При справедливой блокировке, вновь запрашивающие потоки помещаются в очередь, если блокировка удерживается другим потоком или, если потоки помещены в очередь на ожидание блокировки; с несправедливой блокировкой, поток помещается в очередь только в том случае, если блокировка удерживается в текущий момент
    142
    Разве мы не хотим, чтобы все блокировки были справедливыми? В конце концов, справедливость - это хорошо, а несправедливость - плохо, не так ли?
    (Просто спросите своих детей.) Однако, когда дело доходит до блокировки, справедливость приводит к значительным затратам производительности, из-за накладных расходов на приостановку и возобновление потоков. На практике гарантия статистической справедливости - это обещание, что заблокированный поток в конечном счёте захватит блокировку - часто достаточно хороша и затраты на её предоставление значительно дешевле. Некоторые алгоритмы полагаются на справедливую очередь для обеспечения своей корректности, но обычно так не делается. В большинстве случаев, преимущества несправедливой блокировки перевешивают преимущества справедливого помещения в очередь.
    На рис. 13.2 демонстрируется запуск еще одного теста производительности реализации
    Map
    , на этот раз сравнивается производительность экземпляра
    HashMap
    , обёрнутого со справедливой и не справедливой реализацией
    ReentrantLock
    , на четырёх ядерном процессоре Opteron под управлением ОС Solaris, с результатом выведенном по логарифмической шкале
    143
    Штраф за использование
    140
    Когда мы начинали работу над этой книгу, класс
    ReentrantLock казался последним словом в масштабируемости блокировок. Менее чем год спустя, внутренняя блокировка предоставляет хорошую производительность за свои деньги. Производительность - это не просто изменяющийся показатель, она может быть быстро изменяющимся показателем.
    141
    Баржирование – перевозка на барже, в данном контексте перенос вне очереди.
    142
    Опрашиваемый метод tryLock всегда баржируется, даже для справедливой блокировки.
    143
    График для
    ConcurrentHashMap довольно волнистый в области между четырьмя и восемью потоками. Эти колебания почти наверняка возникают из-за измерительного шума, который может быть введен случайными взаимодействиями с хэш-кодами элементов, планированием потоков, изменением размера Map, сборкой мусора или другими эффектами системы памяти или ОС, решающей запустить некоторую периодическую задачу по уборке во время выполнения тестового случая. Реальность такова, что в тестах производительности существуют всевозможные вариации, о контроле которых обычно
    справедливости составляет почти два порядка. Не платите за справедливость, если
    она вам не нужна.
    Рисунок 13.2 Сравнение производительности справедливой и несправедливой блокировок
    Одной из причин, из-за которой баржированные блокировки работают намного лучше, чем справедливые блокировки, при интенсивной конкуренции, является то, что между возобновлением приостановленного потока и его фактическим запуском может быть значительная задержка. Предположим, поток A удерживает блокировку, а поток B запрашивает эту блокировку. Так как блокировка занята, поток B приостанавливается (suspended). Когда поток A освобождает блокировку, выполнение потока B возобновляется (resumed), чтобы он мог повторить попытку.
    Тем временем, однако, если поток C запрашивает блокировку, есть хороший шанс того, что C сможет захватить блокировку, использовать ее и отпустить ещё до того, как поток B закончит просыпаться. В этом случае выигрывают все: поток B захватывает блокировку не позже, чем получил бы её в ином случае, поток C захватывает блокировку намного раньше, а пропускная способность улучшается.
    Справедливые блокировки, как правило, работают лучше всего, когда они удерживаются в течение относительно длительного времени или когда среднее время между запросами на захват блокировки относительно велико. В этих случаях условие, при котором баржирование обеспечивает преимущество в пропускной способности - когда блокировка освобождена, но поток в настоящее время просыпается, чтобы заявить на неё права - удержится с меньшей вероятностью.
    Как и в настройке класса
    ReentrantLock по умолчанию, встроенная блокировка не даёт никаких детерминированных гарантий справедливости, но статистические гарантии справедливости большинства реализаций блокировки достаточно хороши практически для всех ситуаций. Спецификация языка не требует, чтобы JVM реализовывала встроенные блокировки справедливо, и никакие производственные
    JVM этого не делают. Класс
    ReentrantLock не угнетает справедливость блокировки до новых минимумов - он только делает явным то, что присутствовало всё время. беспокоиться не стоит. Мы не пытались искусственно очистить наши графики, потому что реальные измерения производительности также полны шума.

    13.4 Выбор между synchronized и ReentrantLock
    Класс
    ReentrantLock предоставляет ту же семантику блокировки и памяти, что и внутренняя блокировка, а также дополнительные функции, такие как ожидание ограниченной по времени блокировки, прерываемое ожидание блокировки, справедливость и возможность реализации не блочной структурированной блокировки. Производительность класса
    ReentrantLock
    , по всей видимости, доминирует над внутренней блокировкой, немного выигрывая в Java 6 и значительно в Java 5.0. Так почему бы не объявить устаревшим блок synchronized и не рекомендовать всему новому параллельному коду использовать класс
    ReentrantLock
    ? Некоторые авторы фактически и предложили это, рассматривая блок synchronized как “унаследованную” конструкцию. Но это зашло слишком далеко.
    Внутренние блокировки по-прежнему имеют значительные преимущества по сравнению с явными блокировками. Нотация знакома и компактна, и многие существующие программы уже используют встроенную блокировку - и смешивание этих двух видов блокировок может привести к путанице и седлать код подверженным ошибкам. Класс
    ReentrantLock
    , безусловно, более опасный в использовании инструмент, чем синхронизация; если вы забыли обернуть вызов метода unlock блоком finally
    , ваш код, вероятно, будет работать должным образом, но фактически вы создали бомбу замедленного действия, которая может повредить невинным наблюдателям. Приберегите класс
    ReentrantLock для ситуаций, в которых вам нужно что-то обеспечиваемое возможностями класса
    ReentrantLock
    , чего не делает встроенная блокировка.
    Класс
    ReentrantLock представляет собой продвинутый инструмент, применяемый в тех ситуациях, в которых применение внутренних блокировок не практично. Используйте его, если вам нужны расширенные функции: ограничение по времени, опрос, или прерывание захвата блокировки, справедливое помещение в очередь, или не блочная структура блокировки. В ином случае, отдавайте предпочтение блоку synchronized
    В Java 5.0 встроенная блокировка имеет еще одно преимущество по сравнению с классом
    ReentrantLock
    : дампы потоков показывают кадры, в которых видно какими вызовами какие блокировки были захвачены и могут обнаруживать и идентифицировать потоки, попавшие в состояние взаимоблокировки. Среда JVM ничего не знает о том, какими потоками удерживается экземпляр
    ReentrantLock и поэтому не может помочь в отладке проблем с потоками, использующими класс
    ReentrantLock
    . Это несоответствие устранено в Java 6 путем предоставления интерфейса управления и мониторинга, с помощью которого блокировки могут регистрироваться, позволяя информации об экземпляре блокировки
    ReentrantLock появляться в дампах потоков и, вследствие этого, в других интерфейсах управления и отладки. Доступность этой информации для отладки является существенным, хотя в основном и временным, преимуществом блока synchronized
    ; информация о блокировке в дампах потоков спасла многих программистов от полного ужаса. Не блочная структура класса
    ReentrantLock по- прежнему означает, что захват блокировок не может быть связан с определёнными кадрами стека, как это происходит в случае с внутренними блокировками.
    Улучшения производительности в будущем, вероятно, предпочтут блок synchronized классу
    ReentrantLock
    . Поскольку блокировка synchronized
    встроена в JVM, она может выполнять такие оптимизации, как устранение блокировки для ограниченных потоком объектов блокировки и укрупнение блокировок для устранения синхронизации со встроенными блокировками (см. раздел
    11.3.2
    ); выполнение таких операций с блокировками на основе библиотек кажется гораздо менее вероятным. Если вы не планируете выполнять развёртывание в среде Java 5.0 в обозримом будущем, и у вас есть очевидная потребность в преимуществах масштабируемости, предоставляемых классом
    ReentrantLock на этой платформе, будет плохой идеей предпочесть класс
    ReentrantLock блоку synchronized по соображениям производительности.
    13.5 Блокировки на чтение-запись
    Класс
    ReentrantLock реализует стандартную взаимно-исключающую блокировку: экземпляр
    ReentrantLock может одновременно удерживаться не более чем одним потоком. Но взаимное исключение часто является более строгой дисциплиной блокировки, чем необходимо для сохранения целостности данных, и, таким образом, приводит к большему ограничению параллелизма, чем это необходимо.
    Взаимное исключение представляет собой консервативную стратегию блокировки, которая предотвращает перекрытие операций запись/запись и запись/чтение, но также предотвращает перекрытие операций чтение /чтение. Во многих случаях, структуры данных предназначены “в основном для чтения” - они изменяемы и иногда модифицируются, но большинство обращений к ним происходит только для чтения данных. В этих случаях было бы неплохо ослабить требования к блокировке, чтобы позволить нескольким читателям одновременно получать доступ к структуре данных. До тех пор, пока каждому потоку гарантировано актуальное представление данных, и никакой другой поток не изменяет данные представления, в то время как читатели просматривают его, не будет никаких проблем. Это то, что позволяют получить блокировки на чтение-запись: ресурс может быть доступен одновременно либо нескольким читателям, либо одному писателю, но не обоим сразу.
    Интерфейс
    ReadWriteLock
    , приведённый в листинге 13.6, предоставляет два объекта
    Lock
    - один для чтения и один для записи. Для чтения данных, защищаемых блокировкой
    ReadWriteLock
    , вы должны сначала захватить блокировку на чтение, а для изменения данных, защищаемых блокировкой
    ReadWriteLock вы должны сначала захватить блокировку на запись. Хотя может показаться, что существует две отдельные блокировки, блокировка на чтение и блокировка на запись - это просто разные представления интегрированного объекта блокировки на чтение-запись. public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
    }
    Листинг 13.6 Интерфейс
    ReadWriteLock
    Стратегия блокировки, реализуемая блокировками на чтение-запись, позволяет получать одновременный доступ нескольким читателям, но только одному писателю. Подобно интерфейсу
    Lock
    , интерфейс
    ReadWriteLock допускает множество реализаций, которые могут отличаться производительностью, гарантиями планировщика, преференциями при захвате, справедливостью или семантикой блокировки.

    Блокировки на чтение-запись представляют собой оптимизацию производительности и были разработаны для обеспечения большего параллелизма в определенных ситуациях. На практике блокировки на чтение-запись, применяемые в многопроцессорных системах, могут повысить производительность часто используемых структур данных, предназначенных в основном для чтения; в других условиях они работают несколько хуже, чем монопольные блокировки, из- за большей сложности реализации. Являются ли они улучшением, лучше всего определяется в любой конкретной ситуации, с помощью выполнения профилирования; поскольку блокировка
    ReadWriteLock использует экземпляры
    Lock для чтения и записи заблокированными частями, относительно легко поменять блокировку типа чтение-запись на монопольную, если профилирование покажет, что блокировка на чтение-запись не даёт преимуществ.
    Взаимодействие между блокировками на чтение и на запись допускает ряд возможных реализаций. Некоторым из вариантов реализации для
    ReadWriteLock являются:
    Преимущества при освобождении. Когда писатель освобождает блокировку на запись, и оба, и читатель и писатель помещены в очередь, кому из них следует отдать предпочтение - читателям, писателям или тому, кто спросил первым?
    Баржирование читателя. Если блокировка удерживается читателями, но есть ожидающие писатели, должен ли вновь прибывающим читателям быть предоставлен немедленный доступ, или они должны ожидать в очереди позади писателей? Предоставление читателям возможности баржирования перед писателями повышает параллелизм, но создает риск “голодания” для писателей.
    Реентерабельность. Являются ли блокировки чтения и записи реентерабельными?
    Понижение. Если поток удерживает блокировку на запись, может ли он захватить блокировку на чтение без освобождения блокировки на запись? Это позволило бы писателю “понизиться” до блокировки чтения, не позволяя при этом другим писателям изменять защищенный ресурс в то же самое время.
    Повышение. Можно ли повысить блокировку на чтение до блокировки на запись в предпочтении другим ожидающим читателям или писателям? Большинство реализаций блокировки на чтение-запись не поддерживают повышение, поскольку без явной операции повышения оно подвержено взаимоблокировкам. (Если два читателя попытаются одновременно выполнить повышение до блокировки на запись, никто из них не освободит блокировку на чтение.)
    Класс
    ReentrantReadWriteLock предоставляет реентерабельную семантику блокировки для обеих блокировок. Как и класс
    ReentrantLock
    , класс
    ReentrantReadWriteLock может быть построен несправедливым (по умолчанию) или справедливым. В случае справедливой блокировки предпочтение отдается потоку, который ожидал дольше; если блокировка удерживается читателями и поток запрашивает блокировку на запись, читателям не будет позволяться захватывать блокировку на чтение, пока писатель не будет обслужен и не освободит блокировку на запись. В случае несправедливой блокировки, порядок в котором потокам предоставляется доступ, не определен. Понижение от писателя до
    читателя допускается; повышение с читателя до писателя нет (попытка выполнить это приведёт к взаимоблокировке).
    Как и в классе
    ReentrantLock
    , блокировка на запись в классе
    ReentrantReadWriteLock имеет уникального владельца и может быть освобождена только тем потоком, который ее захватил. В Java 5.0 блокировка на чтение ведет себя скорее как семафор, чем как блокировка, поддерживая только количество активных читателей, но не их идентичности. Это поведение было изменено в Java 6, чтобы отслеживать, каким потокам были предоставлены блокировки на чтение.
    144
    Блокировки на чтение-запись могут улучшать параллелизм в тех случаях, когда блокировки обычно удерживаются в течение умеренно длительного времени, и большинство операций не изменяют защищённые ресурсы. Класс
    ReadWriteMap приведённый в листинге 13.7, использует класс
    ReentrantReadWriteLock
    , чтобы обернуть экземпляр
    Map таким образом, чтобы он мог безопасно использоваться несколькими читателями и при этом предотвращать конфликты типа чтение/запись или запись/запись.
    145
    На самом деле производительность класса
    ConcurrentHashMap настолько высока, что вы, вероятнее всего, будете использовать его вместо приведённого подхода, если всё, что вам нужно, это параллельная основанная на хэше реализация Map, но этот метод будет полезен, если вы хотите предоставить больший параллелизм при доступе к альтернативной реализации
    Map
    , такой как
    LinkedHashMap public class ReadWriteMap { private final Map map; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock r = lock.readLock(); private final Lock w = lock.writeLock(); public ReadWriteMap(Map map) { this.map = map;
    } public V put(K key, V value) {
    w.lock(); try { return map.put(key, value);
    } finally {
    w.unlock();
    }
    }
    // Do the same for remove(), putAll(), clear()
    public V get(Object key) {
    r.lock(); try { return map.get(key);
    } finally {
    144
    Одной из причин этого изменения является то, что в Java 5.0 реализация блокировки не может различать поток, запрашивающий блокировку чтения в первый раз от повторного запроса блокировки, что делает справедливые блокировки на чтение-запись подверженными взаимоблокировкам.
    145
    Класс
    ReadWriteMap не реализует интерфейс
    Map
    , потому что реализация методов представления, таких как entrySet и values
    , довольно сложна, и, как правило, “простых” методов обычно достаточно.

    r.unlock();
    }
    }
    // Do the same for other read-only Map methods
    }
    Листинг 13.7 Обёртывание реализации
    Map с помощью блокировки на чтение-запись
    На рисунок 13.3 приведено сравнение пропускной способности класса
    ArrayList
    , обернутого с помощью блокировки
    ReentrantLock и с помощью блокировки
    ReadWriteLock, выполненное в четырех-ядерной системе Opteron под управлением ОС Solaris. Тестовая программа, используемая здесь, похожа на тест производительности реализации
    Map
    , который мы использовали на протяжении всей книги - каждая операция случайным образом выбирает значение и ищет его в коллекции, а небольшой процент от общего количества операций изменяет содержимое коллекции.
    Рисунок 13.3 Производительность блокировки на чтение-запись
    13.6 Итоги
    Явные реализации
    Lock предлагают расширенный набор функций по сравнению со встроенной блокировкой, включая большую гибкость при работе с недоступностью блокировки и больший контроль над поведением очереди. Но класс
    ReentrantLock не является полной заменой блока synchronized
    ; используйте его только тогда, когда вам нужны те функции, которых у блока synchronized нет.
    Блокировки на чтение-запись позволяют нескольким читателям параллельно обращаться к защищаемому объекту, обеспечивая возможность улучшения масштабируемости при доступе к структурам данных, использующим преимущественно чтение.

    1   ...   22   23   24   25   26   27   28   29   ...   34


    написать администратору сайта