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

  • 3.1.1 Устаревшие данные

  • @GuardedBy("this")

  • 3.1.2 Неатомарные 64 битные операции

  • 3.1.3 Блокировки и видимость

  • 3.1.4 Volatile переменные

  • Поток A y = 1 lock M x = 1 unlock M lock M i = x unlock M i = y Поток B

  • 3.2 Публикация

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


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница4 из 34
    1   2   3   4   5   6   7   8   9   ...   34
    Глава 3 Совместно используемые
    объекты
    В начале главы 2 мы заявили, что написание корректных параллельных программ в первую очередь касается управления доступом к разделяемому, изменяемому состоянию. В прошлой главе речь шла об использовании синхронизации для предотвращения одновременного доступа нескольких потоков к одним и тем же данным; в этой главе рассматриваются методы совместного использования
    (sharing)
    31
    и публикации объектов, чтобы они могли быть безопасно доступны из нескольких потоков. Вместе они закладывают основу для создания потокобезопасных классов, и для безопасного структурирования параллельных приложений с использованием классов библиотеки java.util.concurrent
    Мы видели, как синхронизированные блоки и методы могут гарантировать, что операции выполняются атомарно, но существует распространенное заблуждение о том, что оператор synchronized касается только описания атомарности или разграничения «критических секций». Синхронизация также имеет другой важный и тонкий аспект: видимость памяти. Мы хотим не только запретить одному потоку изменять состояние объекта, когда он используется другим потоком, но и гарантировать, что когда поток изменит состояние объекта, другие потоки смогут
    видеть фактически внесенные изменения. Но без синхронизации, этого может не произойти. Вы можете обеспечить безопасную публикацию объектов либо с помощью явной синхронизации, либо путем использования синхронизации, встроенной в классы библиотек.
    3.1 Видимость
    Видимость – понятие тонкое, потому что вещи, которые могут пойти не так, противоречат здравому смыслу. В однопоточной среде, если вы пишете значение в переменную и позже читаете значение этой переменной без промежуточных записей, вы можете ожидать, что получите то же самое значение обратно. Это кажется естественным. Сначала может быть сложно это принять, но когда операции чтения и записи происходят в разных потоках, это не так. В общем, нет никакой гарантии, что читающий поток увидит значение, написанное другим потоком, своевременно или даже вообще. Чтобы обеспечить видимость записи в память между потоками, вы должны использовать синхронизацию.
    В классе
    NoVisibility в листинге 3.1 показано, что может пойти не так, когда потоки совместно используют разделяемые данные без синхронизации. Два потока, главный поток и поток читатель, получают доступ к разделяемым переменным ready и number
    . Главный поток запускает поток-читатель и затем устанавливает переменной number значение 42, а переменной ready значение true
    . Поток читатель будет прокручиваться до тех пор, пока не увидит, что переменная ready имеет значение true
    , и затем напечатает значение переменной number
    . Хотя может показаться очевидным, что класс
    NoVisibility будет печатать 42, на самом деле, возможно, что он будет печатать ноль или вообще никогда не завершит свою работу! Поскольку он не использует соответствующую синхронизацию, нет
    31
    Разделения доступа к объекту между несколькими потоками.
    никакой гарантии, что значения переменных ready и number
    , записанные основным потоком, будут видны потоку-читателю. public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready)
    Thread.yield();
    System.out.println(number);
    }
    } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true;
    }
    }
    Листинг 3.1 Совместное использование переменных без синхронизации. Не делайте так.
    Класс
    NoVisibility может зацикливаться навсегда, потому что значение переменной ready может никогда не стать видимым для потока читателя. Еще более странно, что класс
    NoVisibility может напечатать ноль, потому что запись в переменную ready может быть сделана видимой для потока читателя до записи в переменную number
    , это явление известно как переупорядочение(reordering). Нет никакой гарантии, что операции в одном потоке будут выполняться в порядке заданном программой, при условии, что переупорядочение не будет обнаружено изнутри этого потока - даже если переупорядочение будет очевидным для других
    потоков
    32
    . Когда главный поток, без использования синхронизации, сперва пишет значение в переменную number
    , а затем в переменную ready
    , поток читатель может увидеть, что эти записи происходят в обратном порядке - или нет.
    В отсутствие синхронизации компилятор, процессор и среда выполнения могут делать некоторые совершенно странные вещи с порядком выполнения операций. Попытки рассуждать о порядке, в котором “должны” происходить действия с памятью, в недостаточно синхронизированных многопоточных программах почти наверняка будут неправильными.
    Класс
    NoVisibility примерно так же просто, как и параллельная программа, может получить два потока и две общие переменные - и все же все еще слишком легко прийти к неправильным выводам о том, что он делает или даже будет ли
    32
    Такое решение может показаться признаком плохого дизайна, но это позволяет JVM полностью использовать преимущества современных многопроцессорных систем. Например, при отсутствии синхронизации модель памяти Java позволяет компилятору переупорядочить операции и значения кэша в регистрах и позволяет процессорам переупорядочивать операции и значения кэша в кэшах, специфичных для процессора. Подробнее см. главу 16.
    завершён. Рассуждения о недостаточно синхронизированных параллельных программах непомерно сложны.
    Все это может показаться немного пугающим, и так и должно быть. К счастью, есть простой способ избежать этих сложных проблем: всегда используйте правильную синхронизацию при совместном использовании данных в потоках.
    3.1.1 Устаревшие данные
    Класс
    NoVisibility продемонстрировал один из путей, следуя которым недостаточно синхронизированные программы могут получать неожиданные результаты: устаревшие данные (stale data). Когда поток-читатель проверяет переменную ready
    , он может увидеть устаревшее значение. Если синхронизация не используется каждый раз при доступе к переменной, с некоторой долей вероятности можно увидеть устаревшее значение этой переменной. Хуже того, устаревание это не “всё или ничего”: поток может видеть обновленное значение одной переменной, но также и устаревшее значение другой переменной, которая была записана первой.
    Когда еда несвежая, она обычно все ещё съедобна - просто менее приятна. Но устаревшие данные могут быть более опасными. В то время как устаревший счетчик посещений в веб-приложении может быть не так уж плох
    33
    , устаревшие значения могут вызвать серьезные сбои безопасности или живучести. В классе
    NoVisibility устаревшие значения могут привести к тому, что будет напечатано неверное значение или выполнение программы не сможет быть прервано. Еще сложнее обстоят дела с устаревшими значениями ссылок на объекты, такими как указатели ссылок в реализации связанного списка. Устаревшие данные могут
    вызывать серьезные и запутанные сбои, такие как непредвиденные исключения,
    повреждение структур данных, неточные вычисления и бесконечные циклы.
    Класс
    MutableInteger в листинге 3.2 не потокобезопасен, потому что значение поля value доступно из обоих методов get и set без синхронизации. Среди прочих опасностей, он подвержен устареванию значений: если один поток вызывает set
    , другие потоки, вызывающие get
    , могут видеть или не видеть это обновление.
    @NotThreadSafe public class MutableInteger { private int value; public int get() { return value; } public void set(int value) { this.value = value; }
    }
    Листинг 3.2 Непотокобезопасный изменяемый холдер типа Integer.
    Мы можем сделать класс
    MutableInteger потокобезопасным, синхронизировав геттер и сеттер, как показано в классе
    SynchronizedInteger в листинге 3.3.
    Синхронизация только сеттера будет недостаточной: потоки, вызывающие метод get
    , все равно смогут видеть устаревшие значения.
    33
    Чтение данных без синхронизации аналогично использованию уровня изоляции
    READ_UNCOMMITTED
    в базе данных, где вы готовы разменивать точность на производительность. Однако, в случае использования несинхронизированных операций чтения, вы теряете большую степень точности, так как видимое значение разделяемой переменной может устареть в любой момент времени.

    @ThreadSafe public class SynchronizedInteger {
    @GuardedBy("this") private int value; public synchronized int get() { return value; } public synchronized void set(int value) { this.value = value; }
    }
    Листинг 3.3 Потокобезопасный изменяемый холдер типа
    Integer
    3.1.2 Неатомарные 64 битные операции
    Когда поток читает переменную без синхронизации, он может увидеть устаревшее значение, но, по крайней мере, он видит значение, которое было фактически помещено туда каким-то потоком, а не какое-то случайное значение. Эта гарантия безопасности называется “ безопасностью из неоткуда” (out-of-thin-air safety).
    Безопасность “из ниоткуда” применяется ко всем переменным, с одним исключением: 64 битные числовые переменные (
    double и long
    ), не объявленные с ключевым словом volatile
    (см. раздел
    3.1.4
    ). Модель памяти Java требует, чтобы операции выборки и хранения были атомарными, но для не volatile переменных типа long и double
    JVM разрешено рассматривать 64 битное чтение или запись как две отдельные 32 битные операции. Если операции чтения и записи происходят в разных потоках, существует вероятность прочитать не volatile переменную типа long и в результате получить старшие 32 бита от одного значения и младшие 32 бита от другого
    34
    . Таким образом, даже если вы не заботитесь об устаревании значений переменных, небезопасно использовать разделяемые переменные типа long или double в многопоточных программах, если они не объявлены с ключевым словом volatile или не защищены блокировкой.
    3.1.3 Блокировки и видимость
    Внутреннюю блокировку можно использовать для того, чтобы гарантировать, что один поток видит изменения внесённые другим потоком в предсказуемом виде, как показано на рисунке 3.1. Когда поток A выполняет синхронизированный блок, и впоследствии поток B входит в блок synchronized
    , защищаемый той же блокировкой, значения переменных, которые были видны потоку A до освобождения блокировки, гарантированно будут видны потоку B после захвата блокировки. Другими словами, всё в потоке A, выполненное внутри или до блока synchronized
    , видимо для потока B, когда он выполняет блок synchronized
    , защищенный одной и той же блокировкой. Без синхронизации таких гарантий
    нет.
    Теперь мы можем привести другую причину следовать правилу, требующему, чтобы все потоки синхронизировались на одной и той же блокировке при доступе к разделяемой изменяемой переменной - чтобы гарантировать, что значения, записанные одним потоком, станут видимыми для других потоков. В противном случае, если поток читает переменную, не удерживая соответствующую блокировку, он может увидеть устаревшее значение.
    34
    В то время, когда была написана спецификация виртуальной машины Java, множество широко используемых процессорных архитектур не могли эффективно выполнять атомарные 64-разрядные арифметические операции.

    Рисунок 3.1 Видимость гарантированная синхронизацией.
    Блокировка - это не просто взаимное исключение, но также и видимость памяти. Чтобы убедиться, что все потоки видят самые последние значения разделяемых изменяемых переменных, потоки чтения и записи должны синхронизироваться на общей блокировке
    3.1.4 Volatile переменные
    Язык Java также предоставляет альтернативную, более слабую форму синхронизации, изменчивые переменные (volatile variables), чтобы гарантировать, что обновления переменной предсказуемо распространяются к другим потокам.
    Когда поле объявлено с ключевым словом volatile
    , компилятор и среда выполнения уведомляются о том, что эта переменная является разделяемой и что операции над ней не должны переупорядочиваться с другими операциями памяти.
    Изменчивые переменные не кэшируются в регистрах или в кэшах, где они скрыты от других процессоров, поэтому чтение изменчивой переменной всегда возвращает самую последнюю запись, сделанную любым потоком.
    Хороший способ восприятия volatile переменных заключается в том, чтобы представить, будто бы они, в грубом приближении, похожи на класс
    Поток A
    y = 1
    lock M
    x = 1
    unlock M
    lock M
    i = x
    unlock M
    i = y
    Поток B
    Все, что было
    до разбло-
    кировки M…
    …видно всем,
    после блоки-
    ровки M.

    SynchronizedInteger из листинга 3.3, в котором операции чтения и записи volatile переменной заменены вызовами get и set
    35
    Тем не менее, доступ к volatile переменной не требует захвата блокировки и поэтому не может привести к блокировке выполняющегося потока, что делает переменную, объявленную как volatile,
    более легковесным механизмом синхронизации, по сравнению с блоком synchronized
    36
    Эффекты видимости volatile переменной выходят за пределы значения самой volatile переменной. Когда поток A записывает значение в volatile переменную, а затем поток B считывает значение этой же переменной, значения всех переменных, которые были видны потоку A перед записью в volatile переменную, становятся видимыми потоку B, после прочтения volatile переменной. Таким образом, с точки зрения видимости памяти, запись volatile переменной похожа на выход из блока synchronized
    , а чтение volatile переменной похоже на вход в блок synchronized
    Однако мы не рекомендуем слишком сильно полагаться на volatile переменные для обеспечения видимости; код, который опирается на volatile переменные для обеспечения видимости произвольного состояния, является более хрупким и его сложнее понять, чем код, который использует блокировки.
    Используйте переменные volatile только в том случае, если они упрощают реализацию и проверку политики синхронизации; избегайте использования переменных volatile, когда корректность проверки потребует тонких рассуждений о видимости. Хороший стиль использования переменных volatile включает обеспечение видимости их собственного состояния, состояния объекта, на который они ссылаются, или указание на то, что произошло важное событие жизненного цикла (например, инициализация или завершение работы).
    В листинге 3.4 приведён типичный пример использования volatile переменных: проверка флага состояния для определения момента выхода из цикла. В этом примере наш “очеловеченный” поток пытается уснуть с помощью проверенного временем метода подсчета овец. Для того, чтобы это пример работал, переменная- флаг asleep должна быть volatile. В ином случае, поток может не заметить установку значения переменной asleep другим потоком
    37
    . Вместо этого, мы могли бы использовать блокировку, чтобы обеспечить видимость изменений в переменной asleep
    , но это сделало бы код более громоздким.
    volatile boolean asleep; while (!asleep)
    35
    Эта аналогия не точна; эффекты видимости памяти немного сильнее, чем у изменчивых (volatile) переменных. См. главу 16.
    36
    Чтение volatile переменных лишь немного затратнее чтения не volatile переменных, в большинстве современных процессорных архитектур.
    37
    Совет по отладке: для серверных приложений всегда указывайте параметр командной строки JVM для переключения в серверный режим, даже для разработки и тестирования. Серверная JVM выполняет больше оптимизаций, чем клиентская JVM, такие как подъем переменных из цикла, которые не изменяются в цикле; код, который может работать в среде разработки (клиентская JVM) может сломаться в среде развертывания (серверная JVM). Например, если мы "забыли" объявить переменную в листинге 3.4, серверная JVM может поднять тест из цикла (превратив его в бесконечный цикл), но клиентская JVM этого не сделает. Бесконечный цикл, который проявляется в разработке, обходится намного дешевле того, который появляется только в продуктиве.
    countSomeSheep();
    Листинг 3.4 Класс CountingSheep
    Переменные volatile удобны, но у них есть ограничения. Наиболее часто переменные volatile используются в качестве флагов завершения, прерывания или состояния, как переменная-флаг asleep в листинге 3.4. Переменные volatile можно также использовать для получения других типов сведений о состоянии, но при этом требуется уделять больше внимания. Например, семантика volatile недостаточно сильна, чтобы сделать операцию инкремента (
    count++) атомарной, если только вы не можете гарантировать, что переменная записывается только из одного потока.
    (Атомарные переменные обеспечивают атомарную поддержку чтения-изменения- записи и часто могут использоваться как "лучшие volatile переменные"; см. главу
    15
    .)
    Блокировки могут гарантировать и видимость, и атомарность; volatile переменные только видимость.
    Переменные volatile можно использовать только при соблюдении следующих условий:
    • Запись в переменную не зависит от ее текущего значения, или вы можете гарантировать, что только один поток обновляет значение;
    • Переменная не участвует в инвариантах с другими переменными состояния;
    • Блокировка не требуется по какой-либо другой причине во время обращения к переменной.
    3.2 Публикация и побег
    Публикация (publishing) объекта означает, что он доступен для кода вне его текущей области видимости, например, путем сохранения ссылки на него там, где другой код может найти её, или путём возвращения его из неприватного
    (nonprivate) метода, или путём передачи его методу в другом классе. Во многих ситуациях мы хотим иметь гарантию того, что объекты и их внутренние компоненты не будут публиковаться. В других ситуациях мы хотим опубликовать объект для общего использования, но это может потребовать использования синхронизации. Публикация внутренних переменных состояния может нарушить инкапсуляцию и затруднить сохранение инвариантов; публикация объектов до их полного построения может поставить под угрозу потокобезопасность. Объект, который был опубликован в тот момент, когда этого не должно было произойти, называют
    1   2   3   4   5   6   7   8   9   ...   34


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