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

  • 16.2.1 Небезопасная публикация

  • Листинг 16.3

  • 16.2.2 Безопасная публикация

  • 16.2.3 Идиомы безопасной инициализации

  • = new Resource()

  • ResourceHolder

  • 16.3 Безопасность инициализации

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


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница33 из 34
    1   ...   26   27   28   29   30   31   32   33   34
    Листинг 16.2 Внутренний класс класса
    FutureTask
    , иллюстрирующий комбинированную синхронизацию
    Мы называем такой подход “комбинированным”, потому что он использует существующий порядок, определяемый отношением happens-before, которое было создано по какой-то другой причине, для обеспечения видимости объекта X, вместо создания упорядочивающего отношения happens-before специально для публикации объекта X.
    Вид комбинирования, используемого в классе
    FutureTask довольно хрупок и не должен применяться по случаю. Тем не менее, в некоторых случаях применение комбинирования вполне разумно, например, когда класс фиксирует упорядочивающее отношение happens-before между методами, как часть своей спецификации. Например, безопасная публикация с использованием класса
    BlockingQueue представляет собой форму комбинирования. Один поток помещает объект в очередь, а другой поток впоследствии извлекает его – такая ситуация представляет собой безопасную публикацию, поскольку реализацией класса
    BlockingQueue обеспечивается достаточная внутренняя синхронизация, чтобы гарантировать, что операция помещения в очередь связана отношением happens-
    before с операцией извлечения объекта из очереди.
    Другие отношения упорядочивания happens-before, обеспечиваемые библиотекой классов:
    • Помещение элемента в потокобезопасную коллекцию связано отношением
    happens-before с другим потоком, извлекающим этот элемент из коллекции;
    • Уменьшение счётчика в экземпляре
    CountDownLatch связано отношением
    happens-before с потоком, возвращаемым из метода await этой защёлки;
    • Освобождение разрешения экземпляра
    Semaphore связано отношением
    happens-before с захватом разрешения того же самого экземпляра
    Semaphore
    ;
    • Действия, выполняемые задачей, представленной экземпляром
    Future
    , связаны отношением happens-before с другим потоком, успешно возвращённым из метода
    Future.get
    ;
    • Отправка экземпляров
    Runnable или
    Callable экземпляру
    Executor связана отношением happens-before с началом выполнения задачи; и
    • Поток, прибывший к барьеру
    CyclicBarrier или двустороннему барьеру
    Exchanger
    , связан отношением happens-before с другими потоками, освободившимися у того же барьера или точки обмена. Если класс
    CyclicBarrier использует действие барьера, потоки, прибывающие к барьеру,
    связаны отношением
    happens-before
    с действием барьера, которое, в свою очередь, связано отношением happens-before с потоком, освободившимся у барьера.
    16.2 Публикация
    Ранее, в главе
    3
    , мы рассматривали, как объект может быть опубликован безопасно или неправильно. Описанные там методы безопасной публикации основаны на гарантиях, предоставляемых JMM; риски неправильной публикации являются следствием отсутствия упорядочивающего отношения happens-before между публикацией совместного используемого объекта и доступом к нему из другого потока.
    16.2.1 Небезопасная публикация
    Возможность переупорядочивания при отсутствии связи happens-before объясняет, почему публикация объекта без надлежащей синхронизации может позволить другому потоку увидеть частично созданный объект (см. раздел
    3.5
    ).
    Инициализация нового объекта включает в себя запись в переменные - поля нового объекта. Аналогично, публикация ссылки включает в себя запись в другую переменную - ссылку на новый объект. Если вы не гарантируете, что при публикации совместно используемой ссылки happens-before, другой поток загрузит эту совместно используемую ссылку, то запись ссылки на новый объект может быть переупорядочена (с точки зрения потока, потребляющего объект) с записями в его поля. В этом случае, другой поток может видеть обновленное значение для ссылки на объект, но также видеть устаревшие значения для некоторых или всех
    состояний этого объекта - частично созданный объект.
    Небезопасная публикация может произойти в результате неправильной отложенной инициализации, как показано в листинге 16.3. На первый взгляд, единственной проблемой здесь кажется состояние гонки, описанное в разделе
    2.2.2
    При определенных обстоятельствах, например, когда все экземпляры
    Resource идентичны, вы можете игнорировать их (наряду с возможной неэффективностью создания экземпляра
    Resource более одного раза). К сожалению, даже если эти дефекты игнорируются, небезопасная отложенная инициализация все равно небезопасна, поскольку другой поток может наблюдать ссылку на частично созданный экземпляр
    Resource
    @NotThreadSafe public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { if (resource == null) resource = new Resource(); // unsafe publication return resource;
    }
    }
    Листинг 16.3 Небезопасная отложенная инициализация. Не делайте так.
    Предположим, что поток A первым вызывает метод getInstance
    . Он видит, что объект resource имеет значение null
    , создает новый экземпляр
    Resource и
    устанавливает объекту resource ссылку на него. Когда поток B позже вызывает метод getInstance
    , он мог бы видеть, что ресурс уже имеет ненулевое значение и просто использует уже построенный ресурс. Поначалу это может показаться безобидным, но отношения порядка happens-before между записью экземпляра
    resource
    потоком A и чтением экземпляра
    resource
    потоком B не существует.
    Гонка данных использовалась для публикации объекта, и поэтому потоку B не гарантируется, что он увидит корректное состояние объекта
    Resource
    Конструктор экземпляра
    Resource изменяет поля только что выделенного экземпляра
    Resource со значений по умолчанию (записанных конструктором
    Object
    ) на исходные значения. Так как ни один поток не использовал синхронизацию, поток B мог видеть действия A в порядке отличном от того, в котором A фактически выполнял их. Таким образом, даже если поток A инициализировал экземпляр
    Resource перед установкой resource для ссылки на него, поток B мог видеть запись в ресурс как происходящую перед записями в поля ресурса. Таким образом, поток B может видеть частично созданный ресурс, который вполне может находиться в недопустимом состоянии - и состояние которого может неожиданно измениться позже.
    За исключением неизменяемых объектов, небезопасно использовать объект, инициализированный другим потоком, если только публикация не используется потребляющим потоком с отношением happens-before.
    16.2.2 Безопасная публикация
    Идиомы безопасной публикации, описанные в главе
    3
    , гарантируют, что опубликованный объект виден другим потокам, поскольку они гарантируют, что при публикации с отношением happens-before, потребляющий поток загрузит ссылку на опубликованный объект. Если поток A помещает объект X в экземпляр
    BlockingQueue
    (и иные потоки впоследствии не изменяют его), и поток B извлекает его из очереди, поток B гарантированно увидит объект X, когда поток A оставит его. Это вызвано тем, что реализации
    BlockingQueue имеют достаточную внутреннюю синхронизацию, чтобы гарантировать, что метод put связан отношением happens-before с методом take
    . Аналогичным образом, с помощью совместно используемой переменной, защищаемой блокировкой или совместно используемой переменной volatile
    , гарантируется, что чтение и запись переменной упорядочено отношением happens-before.
    Отношение happens-before фактически даёт более сильные гарантии видимости и упорядоченности, чем можно добиться безопасной публикацией. Когда объект X безопасно публикуется от потока A к потоку B, безопасная публикация гарантирует видимость состояния объекта X, но не состояние других переменных потока A, которых возможно, коснулись. Но если поток A помещает объект X в очередь, связанную отношением happens-before с потоком B, получающим объект X из этой очереди, поток B не только видит объект Х в состоянии, в котором его оставил поток A (при условии, что объект Х не был впоследствии изменен потоком A или еще кем-либо), но поток B также видит всё, что делал поток A до передачи (опять же, с той же оговоркой).
    177
    Почему мы так сильно сосредоточились на аннотации
    @GuardedBy и безопасной публикации, когда спецификация JMM уже предоставляет нам более мощное
    177
    Спецификация JMM гарантирует, что поток B видит значение, по крайней мере, таким же актуальным, как и значение, записанное A; последующие записи могут быть или не быть видимыми.
    отношение happens-before? Мышление с точки зрения передачи права собственности на объект и публикации лучше вписывается в большинство программных проектов, чем мышление с точки зрения видимости отдельных записей в память. Упорядочивающее отношение happens-before работает на уровне отдельных обращений к памяти - это своего рода “язык ассемблера параллелизма”.
    Безопасная публикация работает на уровне, более близком к дизайну вашей программы.
    16.2.3 Идиомы безопасной инициализации
    Иногда имеет смысл отложить инициализацию дорогостоящих объектов до тех пор, пока они действительно не понадобятся, но ранее мы уже сталкивались с тем, что неправильное использование отложенной инициализации может привести к проблемам. Класс
    UnsafeLazyInitialization может быть исправлен путем объявления метода getResource как synchronized
    , как показано в листинге 16.4.
    Поскольку ветка кода, выполняющаяся в методе getInstance
    , является довольно короткой (проверка и прогнозируемая ветвь), и если метод getInstance вызывается множеством потоков достаточно редко, уровень конкуренции за блокировку
    SafeLazyInitialization будет низким, так что этот подход обеспечит адекватную производительность.
    @ThreadSafe public class SafeLazyInitialization private static Resource resource; public synchronized static Resource getInstance() { if (resource == null) resource = new Resource(); return resource;
    }
    }
    Листинг 16.4 Потокобезопасная ленивая (отложенная) инициализация
    Обработка статических полей с инициализаторами (или полей, значения которых инициализируется в статическом блоке инициализации [JPL 2.2.1 и 2.5.3]) несколько специфична и предлагает дополнительные гарантии потокобезопасности. Статические инициализаторы запускаются JVM во время инициализации класса, после загрузки класса, но до того, как класс будет использоваться любым потоком. Поскольку JVM захватывает блокировку во время инициализации [JLS 12.4.2], и эта блокировка захватывается каждым потоком, по крайней мере, один раз, чтобы гарантировать, что класс был загружен, записи в память, сделанные во время статической инициализации, автоматически видны всем потокам. Таким образом, статически инициализированные объекты не требуют явной синхронизации ни во время построения, ни при ссылке. Однако это относится только к состоянию as-constructed - если объект является изменяемым, синхронизация по-прежнему требуется как для чтения, так и для записи, чтобы сделать последующие изменения видимыми и избежать повреждения данных.
    Использование ранней (eager) инициализации, показанной в листинге 16.5, исключает затраты на синхронизацию при каждом вызове метода getInstance класса
    SafeLazyInitialization
    . Этот метод может быть объединен с отложенной
    загрузкой класса средой JVM, для создания метода отложенной инициализации, который не требует синхронизации в общей ветки выполнения кода.
    @ThreadSafe public class EagerInitialization { private static Resource resource = new Resource(); public static Resource getResource() { return resource; }
    }
    Листинг 16.5 Ранняя инициализация.
    Идиома ленивой инициализации классом холдером [EJ Item 48], приведённым в листинге 16.6, использует класс, единственной задачей которого является инициализация экземпляра
    Resource
    . Среда JVM откладывает инициализацию класса
    ResourceHolder до его фактического использования [JLS 12.4.1], и поскольку объект
    Resource инициализируется статическим инициализатором, дополнительная синхронизация не требуется. Первый же вызов метода getResource любым потоком, вынуждает класс
    ResourceHolder загрузиться и инициализироваться, в это время происходит инициализация объекта
    Resource через статический инициализатор.
    @ThreadSafe public class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource();
    } public static Resource getResource() { return ResourceHolder.resource;
    }
    }
    Листинг 16.6 Идиома ленивой инициализации классом холдером.
    16.2.4 Блокировка с двойной проверкой
    Ни одна книга по параллелизму не будет полной без обсуждения печально известного анти паттерна “блокировка с двойной проверкой” (double-checked
    locking, DCL), приведённого в листинге 16.7. В очень ранних версиях JVM, синхронизация, даже в отсутствии конкуренции, приводила к значительным затратам производительности. В результате было изобретено множество умных
    (или, по крайней мере, выглядящих таковыми) трюков, для уменьшения влияния синхронизации - некоторые хорошие, некоторые плохие и некоторые уродливые.
    Анти паттерн DCL попадает в категорию “уродливых”.
    @NotThreadSafe public class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) {
    synchronized (DoubleCheckedLocking.class) { if (resource == null) resource = new Resource();
    }
    } return resource;
    }
    }
    Листинг 16.7 Анти паттерн “блокировка с двойной проверкой”. Не делайте так.
    Опять же, поскольку производительность ранних версий JVM оставляла желать лучшего, ленивая инициализация часто использовалась, чтобы избежать потенциально ненужных дорогостоящих операций или для сокращения затрат времени на запуск приложения. Правильно написанный метод отложенной инициализации требует синхронизации. Но в то время синхронизация была медленной и, что более важно, не совсем понятной: аспекты исключения были поняты достаточно хорошо, а аспекты видимости - нет.
    Анти паттерн DCL намеревался предложить лучшее из обоих миров - ленивую инициализацию, без оплаты штрафа за синхронизации в общей ветке кода. Сначала нужно было проверить без синхронизации, нужна ли инициализация, и, если ссылка на ресурс не равна null
    , использовать ее. В противном случае, выполнить синхронизацию и проверить еще раз, инициализирован ли ресурс, гарантируя таким образом, что только один поток фактически инициализирует совместно используемый экземпляр
    Resource
    . Общая ветка кода - получение ссылки на уже созданный ресурс - не использует синхронизацию. И вот в чем проблема: как описано в разделе
    16.2.1
    , поток может увидеть частично построенный экземпляр
    Resource
    Реальная проблема с DCL заключается в предположении, что худшее, что может произойти при чтении ссылки на совместно используемый объект без синхронизации, это ошибочно увидеть устаревшее значение (в этом случае null
    ); в этом случае идиома DCL компенсирует риск, повторяя попытку с блокировкой.
    Но наихудший случай на самом деле значительно хуже - можно увидеть текущее значение ссылки, но устаревшие значения для состояния объекта, что означает, что объект может быть замечен в недопустимом или неправильном состоянии.
    Последующие изменения в JMM (Java 5.0 и более поздние версии) позволили
    DCL работать, если resource объявлен как volatile
    , и влияние этого на производительность мало, так как volatile чтения не намного затратнее не volatile чтений. Тем не менее, полезность этой идиомы в значительной степени уже в прошлом – предпосылки к её возникновению (медленная синхронизация в отсутствии конкуренции, медленный запуск среды JVM), больше не играют роли, что делает ее менее эффективной в качестве оптимизации. Идиома ленивой инициализации с помощью холдера предлагает те же преимущества и легче в понимании.
    16.3 Безопасность инициализации
    Гарантия безопасности инициализации позволяет потокам безопасно совместно использовать, без синхронизации, правильно сконструированные неизменяемые объекты, независимо от способа их публикации, даже если они опубликованы с использованием гонки данных.
    (Это означает, что класс

    UnsafeLazyInitialization фактически безопасен, если экземпляр
    Resource неизменяем.)
    Без безопасной инициализации, кажущиеся неизменяемыми объекты, такие как
    String
    , могут изменить свое значение, если синхронизация не используется ни публикующим потоком, ни потребляющим. Архитектура безопасности основана на неизменности экземпляра
    String
    ; отсутствие безопасности инициализации может создать уязвимости безопасности, позволяющие вредоносному коду обходить проверки безопасности.
    Безопасность инициализации гарантирует, что для правильно построенных
    объектов все потоки будут видеть правильные значения final полей установленных конструктором, независимо от способа публикации объекта.
    Кроме того, любые переменные, которые могут быть достигнуты (reached) через final поле корректно построенного объекта (например, элементы final массива или содержимое экземпляра HashMap, на которое ссылается поле final), также будут гарантированно видны другим потокам.
    178
    Для объектов с final полями, безопасность инициализации запрещает переупорядочивание любой части конструкции с начальной загрузкой ссылки на этот объект. Все записи в final поля, сделанные конструктором, а также в любые переменные, доступные через эти поля, “замораживаются” по завершении конструктора, и любой поток, который получает ссылку на этот объект, гарантированно увидит значение, по крайней мере, такое же актуальное, как и замороженное значение. Записи, инициализирующие переменные доступные через final поля, не переупорядочиваются с операциями, следующими за замораживанием после построения.
    Безопасность инициализации означает, что класс
    SafeStates из листинга 16.8 может быть безопасно опубликован даже через небезопасную отложенную инициализацию или хранение ссылки на экземпляр
    SafeStates в открытом статическом поле без синхронизации, даже если класс не использует синхронизацию и полагается на не потокобезопасный класс
    HashSet
    @ThreadSafe public class SafeStates { private
    1   ...   26   27   28   29   30   31   32   33   34


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