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

  • Листинг 3.7

  • Листинг 3.8

  • 3.3.1 Специальные ограничения потока

  • 3.3.2 Ограничение стека

  • 3.3.3 Класс ThreadLocal

  • 3.4.2 Использование volatile для публикации неизменяемых объектов

  • 3.5 Безопасная

  • 3.5.1 Некорректная публикация: когда хорошие объекты ведут себя плохо

  • 3.5.2 Неизменяемые объекты и безопасность инициализации

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

  • 3.5.4 Фактически неизменяемые объекты

  • 3.5.5 Изменяемые объекты

  • 3.5.6 Безопасное совместное использование объектов

  • Совместный доступ “только на чтение”

  • Совместное использование потокобезопасных объектов.

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


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница5 из 34
    1   2   3   4   5   6   7   8   9   ...   34
    сбежавшим (escaped). В разделе 3.5 рассматриваются идиомы безопасной публикации; прямо сейчас, мы посмотрим, как объект может сбежать.
    Наиболее вопиющей формой публикации является сохранение ссылки в открытом статическом поле, где любой класс и поток могут видеть его, как в листинге 3.5. Метод initialize создает новый объект
    HashSet и публикует его, сохраняя ссылку на него в переменной knownSecrets public static Set knownSecrets; public void initialize() { knownSecrets = new HashSet();

    }
    Листинг 3.5 Публикация объекта. Класс
    Secrets
    Публикация одного объекта может вызвать косвенную публикацию других. Если вы добавите объект класса
    Secret к опубликованному набору объектов knownSecrets
    , вы также опубликуете и объект класса
    Secret
    , потому что любой код сможет перебрать значения объекта класса
    Set и получить ссылку на новый объект класса
    Secret.
    Аналогичным образом, возврат ссылки из не приватного метода также публикует возвращенный объект. Класс
    UnsafeStates в листинге 3.6 публикует якобы приватный массив аббревиатур названий штатов. class UnsafeStates { private String[] states = new String[] {
    "AK", "AL" ...
    }; public String[] getStates() { return states; }
    }
    Листинг 3.6 Предоставление возможности «сбежать» внутреннему изменяемому состоянию. Не делайте так.
    Публикация объекта states,
    таким образом, является проблематичной, потому что любой вызывающий код может изменить его содержимое. В этом случае массив states
    “сбегает” из предполагаемой области видимости, потому что то, чему предполагалось быть приватным, фактически было сделано публичным.
    Публикация объекта также публикует все объекты, на которые ссылаются его публичные поля. Обобщая сказанное, любой объект, который доступен из опубликованного объекта, следуя некоторой цепочке публичных ссылок на поля и вызовы методов, также был опубликован.
    С точки зрения класса C, чужой метод - один из тех, чье поведение не полностью определяется классом C. Это понятие включает в себя методы в других классах, а также переопределяемые методы (ни private
    , ни final
    ) в самом классе
    C. Передача объекта чужому методу, также должна рассматриваться как публикация этого объекта. Поскольку вы не можете предполагать, какой код фактически будет вызван, вы не можете знать, захочет ли чужой метод опубликовать объект или сохранить ссылку на него, которая впоследствии может быть использована из другого потока.
    Действительно ли другой поток сделает что-либо с опубликованной ссылкой, на самом деле не имеет значения, потому что риск неправильного использования существует в любом случае
    38
    . Как только объект убегает, вы должны предположить, что другой класс или поток могут, злонамеренно или по неосторожности, использовать его некорректно. Это является веской причиной для использования инкапсуляции: она позволяет выполнять практический анализ программ на корректность и усложняет случайное нарушение ограничений, заложенных на стадии проектирования
    Последним механизмом, с помощью которого можно опубликовать объект или его внутреннее состояние, является публикация внутреннего экземпляра класса, как показано в классе
    ThisEscape
    , в листинге 3.7. Когда класс
    ThisEscape публикует
    EventListener
    , он также неявно публикует вложенный класс
    38
    Если кто-то украл ваш пароль и поместил его в группе новостей alt.free-passwords
    , эта информация “убежала”: независимо от того, использовал ли кто-либо эти учетные данные для причинения вреда, ваша учетная запись все еще скомпрометирована. Публикация ссылки представляет собой такой же риск.

    ThisEscape
    , потому что экземпляры внутреннего класса содержат скрытую ссылку на вложенный экземпляр. public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e);
    }
    });
    }
    }
    Листинг 3.7 Неявное позволение “сбежать” ссылке на this. Не делайте так.
    3.2.1 Методы безопасного построения
    Класс
    ThisEscape иллюстрирует важный частный случай побега - когда ссылка на this ссылка “сбегает” во время построения. Когда публикуется внутренний экземпляр
    EventListener
    , то же самое относится и к вложенному экземпляру
    ThisEscape
    . Но объект находится в предсказуемом и согласованном состоянии только после возврата из конструктора, поэтому публикация объекта из конструктора может привести к публикации не полностью построенного объекта.
    Это справедливо, даже если публикация выполняется последним выражением в
    конструкторе. Если ссылка на this сбегает в процессе построения, объект считается построенным неправильно
    39
    This is true even if the publication is the last
    statement in the constructor
    Не позволяйте ссылке на this “сбегать” в процессе построения.
    Распространённой ошибкой, позволяющей сбегать ссылке на this во время построения, является запуск потока из конструктора. Когда объект создает поток из своего конструктора, он почти всегда делится своей ссылкой с новым потоком либо явно (передавая его конструктору), либо неявно (поскольку
    Thread или
    Runnable является внутренним классом объекта-владельца). Новый поток сможет увидеть объект-владелец, прежде чем он будет полностью построен. Нет ничего плохого в создании потока в конструкторе, но лучше не запускать поток сразу.
    Вместо этого предоставьте методы start или initialize
    , которые запустят собственный поток. (Дополнительные сведения о проблемах жизненного цикла службы см. в главе 7.) Вызов переопределяемого метода экземпляра (который не является ни private
    , ни final
    ) из конструктора также может позволить сбежать ссылке на this
    Если у вас возникнет соблазн зарегистрировать слушателя событий или запустить поток из конструктора, вы можете избежать неправильного построения с
    39
    Если говорить более конкретно, ссылка на this не должна покидать поток до тех пор, пока конструктор не вернёт управление. Ссылка на this может быть где-нибудь сохранена конструктором, но до завершения построения, она не может быть использована другими потоками. Класс
    SafeListener в листинге 3.8 использует эту технику.
    помощью приватного конструктора и открытого фабричного метода, как показано в классе
    SafeListener в листинге 3.8. public class SafeListener { private final EventListener listener; private SafeListener() { listener = new EventListener() { public void onEvent(Event e) { doSomething(e);
    }
    };
    } public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener(); source.registerListener(safe.listener); return safe;
    }
    }
    Листинг 3.8 Использование фабричного метода для предотвращения побега ссылки на this в процессе построения.
    3.3 Ограничение потока
    Доступ к общим изменяемым данным требует использования синхронизации; один из способов избежать этого требования - не предоставлять общий доступ. Если доступ к данным осуществляется только из одного потока, синхронизация не требуется. Этот метод, ограничение потока, является одним из самых простых способов обеспечения безопасности потоков. Когда объект ограничен потоком, такое использование автоматически потокобезопасно, даже если ограниченный объект сам по себе не потокобезопасен [CPJ 2.3.2]
    40
    Swing использует метод ограничения потока в широких пределах. Визуальные компоненты и модели данных Swing не потокобезопасны; напротив, потокобезопасность достигается путём ограничения доступа к ним только потоком обработки событий Swing. Чтобы правильно использовать Swing, код, выполняющийся в потоке, отличном от потока событий, не должен обращаться к этим объектам. (Чтобы упростить работу, Swing предоставляет механизм invokeLater для назначения
    Runnable на выполнение в потоке событий.)
    Множество ошибок параллелизма в приложениях Swing происходит из-за неправильного использования ограниченных объектов из других потоков.
    Другим распространенным применением ограничения потоков является использование объединенных в пул объектов соединения JDBC (Java Database
    Connectivity)
    Connection
    . Спецификация JDBC не требует, чтобы объекты
    Connection были потокобезопасны.
    41
    В типичных серверных приложениях поток получает соединение из пула, использует его для обработки одного запроса и
    40
    Doug Lea. Concurrent Programming in Java, Second Edition. Addison–Wesley, 2000.
    41
    Реализации пулов соединений, предоставляемые серверами приложений, потокобезопасны; как правило, доступ к пулам соединений осуществляется из нескольких потоков, поэтому реализации, не являющиеся потокобезопасными, не имеют смысла.
    возвращает его. Поскольку большинство запросов, таких как запросы сервлета или вызовы EJB (Enterprise JavaBeans), обрабатываются синхронно одним потоком, и пул не будет передавать одно и то же соединение другим потокам до тех пор, пока оно не будет возвращено, этот шаблон управления соединением неявно ограничивает объект
    Connection этим потоком на время выполнения запроса.
    Так же, как язык не имеет механизмов для принудительного обеспечения того, чтобы переменная была защищена блокировкой, у него нет средств ограничения объекта потоком. Ограничение потоков - это элемент дизайна вашей программы, который должен обеспечиваться его реализацией. Язык и основные библиотеки предоставляют механизмы, которые могут помочь в поддержании ограничения потока - локальные переменные и класс
    ThreadLocal
    - но даже с ними, только программист несёт ответственность за гарантию того, что ограниченные потоком объекты не покинут предназначенного им потока.
    3.3.1 Специальные ограничения потока
    Специальное ограничение потока (Ad-hoc thread confinement) описывает, когда ответственность за поддержание ограничения потока полностью ложится на реализацию. Ограничение потока может быть хрупким, поскольку ни одна из возможностей языка, такая как модификаторы видимости или локальные переменные, не помогает ограничить объект целевым потоком. На самом деле ссылки на объекты, ограниченные потоками, такие как визуальные компоненты или модели данных в приложениях с графическим пользовательским интерфейсом, часто хранятся в открытых полях.
    Решение использовать ограничение потока часто является следствием решения реализовать определенную подсистему, такую как GUI, как однопоточную подсистему. Однопоточные подсистемы могут иногда предложить такое преимущество, как простота, которое перевешивает хрупкость специального ограничения потока
    42
    Частный случай ограничения потока применяется и к volatile переменным.
    Безопасно выполнять операции read-modify-write с использованием общих переменных volatile, пока вы гарантируете, что volatile переменная будет записана только из одного потока. В этом случае вы ограничиваете модификацию только одним потоком, чтобы предотвратить условия гонки, а гарантии видимости volatile переменных устанавливают, что другие потоки будут видеть последнее изменённое значение.
    В связки с хрупкостью, ограничение потока следует использовать крайне скупо; если возможно, используйте одну из более сильных форм ограничения потоков (ограничение стека или
    ThreadLocal
    ).
    3.3.2 Ограничение стека
    Ограничение стека (Stack confinement) является частным случаем ограничения потока, при котором доступ к объекту может быть получен только через локальные переменные. Подобно тому, как инкапсуляция может упростить сохранение инвариантов, локальные переменные могут упростить ограничение объектов потоком. Локальные переменные внутренне ограничены исполняемым потоком; они существуют в стеке исполняемого потока, который недоступен для других
    42
    Еще одна причина сделать подсистему однопоточной - исключение возможности взаимоблокировок; это одна из основных причин того, почему большинство платформ GUI однопоточны. Однопоточные подсистемы описаны в главе 9.
    потоков. Ограничение стека (также называемое внутри-поточным (within-thread) или локально-поточным (thread-local) использованием потока, не путать с библиотечным классом
    ThreadLocal!
    ) проще поддерживать и оно менее хрупко, чем специальное ограничение потока.
    В случае локальных переменных примитивных типов, таких как numPairs в методе loadTheArk в листинге 3.9, вы не сможете нарушить ограничение стека, даже если попробуете. Невозможно получить ссылку на примитивную переменную, поэтому семантика языка гарантирует, что примитивные локальные переменные всегда будут ограничены стеком. public int loadTheArk(Collection candidates) {
    SortedSet animals; int numPairs = 0;
    Animal candidate = null;
    // animals confined to method, don’t let them escape!
    animals = new TreeSet(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a));
    ++numPairs; candidate = null;
    }
    } return numPairs;
    }
    Листинг 3.9 Ограничение потока локальными примитивами и ссылочными переменными в классе
    Animals
    Поддержание ограничения стека для ссылок на объекты требует немного больше помощи от программиста, чтобы гарантировать, что ссылающийся объект не сбежит. В классе loadTheArk мы создаем объект
    TreeSet и храним ссылку на него в переменной animals
    . На данный момент существует только одна ссылка на объект
    Set
    , содержащаяся в локальной переменной и, следовательно, ограничиваемая исполняющим потоком. Однако если бы мы опубликовали ссылку на объект
    Set
    (или любую его внутреннюю часть), то ограничение было бы нарушено, и “животные” убежали бы.
    Использование непотокобезопасного объекта в контексте “внутри потока” по- прежнему является потокобезопасным. Однако будьте осторожны: требование к дизайну, чтобы объект ограничивался исполняемым потоком, или осознание того, что закрытый объект не является потокобезопасным, часто существует только в голове разработчика в момент написании кода. Если допущение о внутрипоточном использовании объекта явно не задокументировано, те, кто будут в будущем сопровождать код, могут по ошибке позволить этому объекту сбежать.

    3.3.3 Класс ThreadLocal
    Более формальным средством поддержания ограничения потока является класс
    ThreadLocal
    , который позволяет связать значение каждого потока с объектом хранения значения. Класс
    ThreadLocal предоставляет методы доступа get и set
    , которые поддерживают отдельную копию значения для каждого потока, который использует его, поэтому get возвращает самое последнее значение, переданное set
    из текущего выполняемого потока.
    Локально-поточные переменные часто используются для предотвращения совместного использования в проектах на основе изменяемых синглтонов или глобальных переменных.
    Например, однопоточное приложение может поддерживать подключение к глобальной базе данных, инициализируемое при запуске, чтобы избежать передачи объекта
    Connection каждому методу. Так как соединения JDBC не могут быть потокобезопасными, многопоточное приложение, которое использует глобальное соединение без дополнительной координации доступа, также не является потокобезопасным. Используя класс
    ThreadLocal для хранения соединения JDBC, как в методе connectionHolder в листинге 3.10, каждый поток будет иметь свое собственное соединение private static ThreadLocal connectionHolder
    = new ThreadLocal() { public Connection initialValue() { return DriverManager.getConnection(DB_URL);
    }
    }; public static Connection getConnection() { return connectionHolder.get();
    }
    Листинг 3.10 Использование
    ThreadLocal для обеспечения ограничения потока.
    Этот метод может также использоваться, когда часто используемая операция требует временного объекта, такого как буфер, и хочет избежать перераспределения временного объекта при каждом вызове. Например, до Java 5.0 метод
    Integer.toString использовал класс
    ThreadLocal для хранения 12- байтового буфера, используемого для форматирования его результата, вместо использования общего статического буфера (который потребовал бы блокировки) или выделения нового буфера для каждого вызова
    43
    Когда поток вызывает метод
    ThreadLocal.get в первый раз, метод initialValue используется для обеспечения потока начальным значением. В концептуальном плане вы можете думать о классе
    ThreadLocal
    как о холдере
    Map
    , который сохраняет значения, зависящие от потока, хотя, на самом деле, это реализовано не так. Значения, специфичные для потока, хранятся в самом объекте
    Thread
    ; когда поток завершается, значения, зависящие от потока, будут убраны “сборщиком мусора”.
    43
    Этот метод вряд ли даст выигрыш производительности, если операции выполняются очень часто или выделение памяти необычайно дорого. В Java 5.0 он был заменен более прямолинейным подходом, заключающимся в выделения нового буфера для каждого вызова, в связи с предположением, что для чего-то столь же обыденного, как временный буфер, он не даст выигрыша производительности.

    Если вы переносите однопоточное приложение в многопоточную среду, вы можете сохранить безопасность потоков путем преобразования общих глобальных переменных в объекты
    ThreadLocal
    , если это позволяет семантика общих глобальных переменных; кэш уровня приложения не был бы столь же полезным, если бы он был размещён в нескольких локально-поточных кэшах..
    Класс
    ThreadLocal широко используется в реализации фрэймворков.
    Например, контейнеры J2EE связывают контекст транзакции (transaction context) с исполняемым потоком на всём протяжении вызова EJB. Это легко реализуется с использованием статического объекта ThreadLocal, содержащего контекст транзакции: когда фреймворк необходимо определить, какая транзакция выполняется в настоящий момент, он извлекает контекст транзакции из объекта
    ThreadLocal
    . Это удобно в том смысле, что снижает необходимость передавать информацию о контексте выполнения в каждый метод, но связывает любой код, который использует этот механизм в фреймворке.
    Легко злоупотребить классом
    ThreadLocal
    , трактуя его свойство ограничения потока как лицензию на использование глобальных переменных или как средство создания “скрытых” аргументов методов. Как и глобальные переменные, поточно- локальные переменные могут отвлекать от повторного использования и вводить скрытые связи между классами, и поэтому их следует использовать с большой осторожностью.
    3.4 Неизменяемость
    Другой конечной целью синхронизации является использование неизменяемых объектов [EJ Item 13]. The other end-run around the need to synchronize is to use
    immutable objects [EJ Item 13]. Почти все опасности атомарности и видимости, которые мы описывали до сих пор, такие как просмотр устаревших значений, потеря обновлений или наблюдение за объектом, находящимся в несогласованном состоянии, связаны с превратностями доступа из нескольких потоков, пытающихся получить доступ к одному и тому же изменяемому состоянию в одно и то же время.
    Если состояние объекта не может быть изменено, эти риски и сложности просто исчезают.
    Неизменяемым (immutable) объектом является объект, состояние которого не может быть изменено после построения. Неизменяемые объекты по своей сути являются потокобезопасными; их инварианты устанавливаются конструктором, и если их состояние не может быть изменено, то эти инварианты всегда удерживаются.
    Неизменяемые объекты всегда потокобезопасны.
    Неизменяемые объекты просты. Они могут находиться только в одном состоянии, которое тщательно контролируется конструктором. Один из самых сложных элементов дизайна программы - рассуждение о возможных состояниях сложных объектов. С другой стороны, рассуждение о состоянии неизменных объектов тривиально.
    Неизменяемые объекты также безопаснее. Передача изменяемого объекта в ненадежный код или публикация в то место, где ненадежный код может найти его
    – опасна. Ненадежный код может изменить состояние объекта или, что еще хуже, сохранить ссылку на него и изменить его состояние позже из другого потока. С другой стороны, неизменяемые объекты не могут быть скомпрометированы с
    помощью вредоносного или ошибочного кода, поэтому они могут безопасно совместно использоваться (share) и публиковаться без необходимости создавать защищённые копии [EJ Item 24].
    Ни спецификация языка Java, ни модель памяти Java формально не определяют неизменность, но неизменяемость не эквивалентна простому объявлению всех полей объекта как final
    . Объект, все поля которого являются final
    , может по- прежнему быть изменяемым, поскольку final поля могут содержать ссылки на изменяемые объекты.
    Объект неизменяемый если:
    • Его состояние не может быть изменено после построения;
    • Все поля объявлены как final
    44
    ;
    • Он правильно построен (ссылка на this не может “сбежать” в процессе построения)).
    Неизменяемые объекты могут по-прежнему использовать изменяемые объекты для управления своим состоянием, как показано в классе
    ThreeStooges в листинге
    3.11. Хотя объект
    Set
    , в котором хранятся имена, изменяемый, дизайн класса
    ThreeStooges не позволяет изменить объект
    Set после построения. Ссылка на переменную stooges помечена как final
    , поэтому всё состояние объекта достигается через поле типа final
    . Последнее требование - правильное построение
    - легко выполняется, поскольку конструктор ничего не делает, чтобы эта ссылка стала доступной для кода, отличного от конструктора и кода, вызвавшего его.
    @Immutable public final class ThreeStooges { private final Set stooges = new HashSet(); public ThreeStooges() { stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly");
    } public boolean isStooge(String name) { return stooges.contains(name);
    }
    }
    Листинг 3.11 Неизменяемый класс построенный с использованием изменяемых объектов.
    Поскольку состояние программы изменяется все время, вы можете испытывать соблазн подумать, что неизменяемые объекты имеют ограниченное использование, но это не так. Существует разница между неизменяемым объектом и
    44
    Технически возможно иметь неизменяемый объект, если не все поля объявлены как final.
    String
    - пример такого класса, но это всё опирается на тонкие рассуждения о “доброкачественных” гонках данных, что требует глубокого понимания модели памяти Java. (Для любопытных:
    String вычисляет хэш-код в ленивой манере при первом вызове метода hashCode и кэширует его в не финальном поле, но это работает только потому, что это поле может принимать только одно значение nondefault
    , которое является одинаковым каждый раз, когда оно вычисляется, потому что оно детерминировано выводится из неизменяемого состояния. Не пытайтесь повторить это дома.)
    неизменяемой ссылкой на него. Состояние программы, сохраненное в неизменяемых объектах, все еще может быть обновлено путем «замены» неизменяемых объектов новыми экземплярами, содержащими новое состояние; в следующем разделе приведен пример использования этой методики
    45
    3.4.1 Поля типа final
    Ключевое слово final
    , более ограниченная версия механизма const из C++, поддерживает построение неизменяемых объектов. Поля типа final не могут быть изменены (хотя объекты, на которые они ссылаются, могут быть изменены, если они изменяемы), но они также имеют специальную семантику в модели памяти
    Java. Именно использование полей типа final делает возможной гарантию
    безопасной инициализации (см. раздел
    3.5.2
    ), которая позволяет получать свободный доступ к неизменяемым объектам без синхронизации.
    Даже если объект является изменяемым, пометка некоторых полей как final может упростить рассуждения о его состоянии, поскольку ограничение изменяемости объекта ограничивает набор возможных состояний объекта. Объект, который является “в основном неизменяемым”, но имеет одну или две изменяемые переменные состояния, по-прежнему проще в понимании, чем тот, который имеет множество изменяемых переменных. Объявление полей final также документирует для сопровождающих, что эти поля не должны изменяться.
    Хорошая практика заключается в том, чтобы делать все поля private, если они не нуждаются в большей видимости [EJ Item 12], также хорошей практикой является сделать все поля final, если они не должны быть изменяемыми.
    3.4.2 Использование volatile для публикации неизменяемых
    объектов
    В классе
    UnsafeCachingFactorizer мы попытались использовать два объекта
    AtomicReference s для хранения последнего числа и последнего фактора, но это было не потокобезопасно, потому что мы не могли получить или обновить оба связанных значения атомарно. Использование volatile переменных для этих значений не будет потокобезопасным по той же причине. Однако неизменяемые объекты могут иногда обеспечивать слабую форму атомарности.
    Сервлет факторинга выполняет две операции, которые должны быть атомарными: обновление кэшированного результата и условная выборка кэшированных факторов, если кэшированный номер соответствует запрошенному номеру. Всякий раз, когда группа связанных элементов данных должна действовать атомарно, рассмотрите возможность создания для них неизменяемого класса- холдера, такого как
    OneValueCache
    46
    в листинге 3.12.
    Условия гонки при доступе или обновлении нескольких связанных переменных можно устранить, используя неизменяемый объект для хранения всех переменных.
    45
    Многие разработчики опасаются, что такой подход вызовет проблемы с производительностью, но эти опасения обычно необоснованны. Выделение памяти дешевле, чем вы могли бы подумать, а неизменяемые объекты предлагают дополнительные преимущества производительности, такие как сокращение потребности в блокировках или защитных копиях и снижение нагрузки на сборщик мусора.
    46
    Класс
    OneValueCache не был бы неизменяемым без вызова метода copyOf в конструкторе и геттере.
    Метод
    Arrays.copyOf был добавлен для удобства в Java 6; метод clone также будет работать.

    @Immutable class OneValueCache { private final BigInteger lastNumber; private final BigInteger[] lastFactors; public OneValueCache(BigInteger i,
    BigInteger[] factors) { lastNumber = i; lastFactors = Arrays.copyOf(factors, factors.length);
    } public BigInteger[] getFactors(BigInteger i) { if (lastNumber == null || !lastNumber.equals(i)) return null; else return Arrays.copyOf(lastFactors, lastFactors.length);
    }
    }
    Листинг 3.12 Неизменяемый холдер, кэширующий число и его фактор.
    С изменяемым объектом holder вам придется использовать блокировку для обеспечения атомарности; с неизменяемым, как только поток получает ссылку на него, ему не нужно беспокоиться о другом потоке, изменяющем его состояние.
    Если переменные должны быть обновлены, создается новый объект holder
    , но все потоки, работающие с предыдущим объектом holder
    , по-прежнему видят его в согласованном состоянии.
    Класс
    VolatileCachedFactorizer в листинге 3.13 использует класс
    OneValueCache для хранения кэшированного числа и факторов. Когда поток устанавливает volatile полю cache ссылку на новый экземпляр класса
    OneValueCache
    , новые кэшированные данные сразу становятся видимыми другим потокам.
    Операции, связанные с кэшем, не могут оказывать влияние друг на друга, поскольку класс
    OneValueCache неизменяемый, и поле cache доступно только единожды в каждой из соответствующих веток кода. Эта комбинация неизменяемого объекта-холдера для нескольких переменных состояния, связанных с инвариантом, и ссылки на поле типа volatile
    , используемой для гарантии своевременной видимости, позволяет классу
    VolatileCachedFactorizer быть потокобезопасным, даже если он не содержит явных блокировок.
    3.5 Безопасная публикация
    До сих пор мы были сосредоточены на обеспечении того, чтобы объект не публиковался, предполагая, что он должен ограничиваться потоком или внутренним состоянием другого объекта. Конечно, иногда мы хотим совместно использовать объекты в разных потоках, и в этом случае мы должны делать это безопасно. К сожалению, простое сохранение ссылки на объект в поле типа public
    , как в листинге 3.14, является не достаточным условием для безопасной публикации объекта.

    @ThreadSafe public class VolatileCachedFactorizer implements Servlet { private volatile OneValueCache cache = new
    OneValueCache(null, null); public void service(ServletRequest req, ServletResponse resp) {
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new OneValueCache(i, factors);
    } encodeIntoResponse(resp, factors);
    }
    }
    Листинг 3.13 Кэширование результата выполнения последней операции с использование volatile ссылки на неизменяемый объект холдера.
    // Unsafe publication
    public Holder holder; public void initialize() { holder = new Holder(42);
    }
    Листинг 3.14 Публикация объекта без адекватной синхронизации. Не делайте так.
    Вы можете удивиться тому, к насколько серьёзным последствиям может привести крах этого безобидно выглядящего примера. Из-за проблем с видимостью класс
    Holder может оказаться в другом потоке в несогласованном состоянии, несмотря на то, что его инварианты были правильно установлены его конструктором! Некорректная публикация может привести к тому, что другие потоки смогут наблюдать частично сконструированный объект.
    3.5.1 Некорректная публикация: когда хорошие объекты
    ведут себя плохо
    Вы не можете полагаться на целостность частично построенных объектов.
    Наблюдающий поток мог видеть объект в несогласованном состоянии, а затем увидеть, что его состояние внезапно изменилось, хотя оно не изменялось после публикации. Фактически, если класс
    Holder в листинге 3.15 опубликован с использованием небезопасной идиомы публикации из листинга 3.14, а поток, отличный от публикующего потока, должен был вызвать метод assertSanity
    , могло быть порождено исключение
    AssertionError
    !
    47 47
    Проблема здесь не в классе
    Holder как таковом, а в том, что класс
    Holder не опубликован должным образом. Однако класс
    Holder может быть защищен от некорректной публикации, путём объявления поля n
    как final
    , что сделает владельца неизменяемым; см. раздел 3.5.2.
    public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false.");
    }
    }
    Листинг 3.15 Класс с заложенной возможностью краха из-за некорректной публикации.
    Поскольку синхронизация не использовалась, чтобы сделать класс
    Holder видимым для других потоков, мы говорим, что класс
    Holder был опубликован
    некорректно. Две вещи могут пойти не так, как ожидалось, с некорректно опубликованными объектами. Другие потоки могли видеть устаревшее значение поля типа holder и, таким же образом, видеть нулевую ссылку или другое более старое значение, даже если значение было помещено в holder
    . Но, что гораздо хуже, другие потоки могли увидеть текущую ссылку на класс holder
    , но устаревшее значение состояния класса holder
    48
    . Чтобы сделать вещи еще менее предсказуемыми, поток может увидеть устаревшее значение при первом чтении поля, а затем более актуальное значение в следующий раз, поэтому метод assertSanity может бросить исключение
    AssertionError
    Рискуя повторить ранее сказанное, отметим, что могут происходить очень странные вещи, когда данные разделяются между потоками без достаточной синхронизации.
    3.5.2 Неизменяемые объекты и безопасность инициализации
    Поскольку неизменяемые объекты настолько важны, модель памяти Java предлагает специальную гарантию безопасности инициализации для совместного использования неизменяемых объектов. Как мы видели ранее, то, что ссылка на объект становится видимой для другого потока, не обязательно означает, что состояние этого объекта видно потребляющему потоку. Чтобы гарантировать согласованное представление состояния объекта, необходима синхронизация.
    С другой стороны, с неизменяемыми объектами можно безопасно обращаться,
    даже если синхронизация не используется для публикации ссылки на объект. Для обеспечения гарантии безопасности инициализации должны соблюдаться все требования к неизменяемости: не модифицируемое состояние, все поля объявлены как final и корректное построение. (Если бы класс
    Holder в листинге 3.15 был неизменяемым, метод assertSanity не смог бы возбудить исключение
    AssertionError
    , даже если класс
    Holder не был опубликован должным образом.)
    Неизменяемые объекты могут безопасно использоваться любым потоком без дополнительной синхронизации, даже если синхронизация не используется для их публикации.
    48
    Хотя может показаться, что значения полей, заданные в конструкторе, являются первыми значениями, записываемыми в эти поля, и поэтому нет «более старых» значений, называемых устаревшими, конструктор класса Object, перед запуском конструкторов подкласса, сперва записывает для всех полей значения по умолчанию. Следовательно, существует вероятность увидеть значение по умолчанию установленное полю, как устаревшее значение.

    Эта гарантия распространяется на значения всех конечных полей правильно построенных объектов; конечные поля могут быть безопасно доступны без дополнительной синхронизации. Однако, если поля типа final ссылаются на изменяемые объекты, синхронизация по-прежнему требуется для доступа к состоянию объектов, на которые они ссылаются.
    3.5.3 Идиомы безопасной публикации
    Объекты, которые не являются неизменяемыми, должны быть безопасно опубликованы, что обычно влечет за собой синхронизацию как публикующего, так потребляющего потоков. На данный момент давайте сосредоточимся на том, что потребляющий поток может видеть объект в его опубликованном состоянии; мы будем иметь дело с видимостью изменений, сделанных в ближайшее, после публикации, время.
    Для безопасной публикации объекта, ссылка на объект и его состояние должны быть одновременно видны другим потокам. Правильно построенный объект может быть безопасно опубликован с помощью:
    • Инициализации ссылки на объект из статического инициализатора;
    • Хранения ссылки на него в поле типа volatile или AtomicReference;
    • Сохранения ссылки на него в поле типа final корректно построенного объекта;
    • Хранения ссылки на него в поле, должным образом защищенным блокировкой.
    Внутренняя синхронизация в потокобезопасных коллекциях означает, что размещение объекта в потокобезопасной коллекции, такой как
    Vector или synchronizedList
    , удовлетворяет последнему из этих требований. Если поток A помещает объект X в потокобезопасную коллекцию и поток B впоследствии извлекает его, поток B гарантированно видит состояние объекта X таким, каким поток A оставил его, даже если код приложения, который передает объект X подобным образом, не имеет явной синхронизации. Потокобезопасные библиотеки коллекций предлагают следующие гарантии безопасной публикации, даже если в
    Javadoc об этом почти ничего не сказано:
    • Размещение ключа или значения в классах
    Hashtable
    , synchronizedMap или
    ConcurrentMap безопасно публикует его в любом потоке, который извлекает его из класса
    Map
    (напрямую или через итератор);
    • Размещение элемента в классах
    Vector
    ,
    CopyOnWriteArrayList
    ,
    CopyOnWriteArraySet
    , synchronizedList
    , или synchronizedSet безопасно публикует его в любой поток, который извлекает его из коллекции;
    • Размещение элемента в
    BlockingQueue или
    ConcurrentLinkedQueue безопасно публикует его в любом потоке, который извлекает его из очереди.
    Другие механизмы передачи состояния в библиотеке классов (такие как
    Future и
    Exchanger
    ) также представляют собой безопасную публикацию; мы определим их как обеспечивающие безопасную публикацию по мере их представления.

    Использование статического инициализатора часто является самым простым и безопасным способом публикации объектов, которые могут быть построены статически: public static Holder holder = new Holder(42);
    Статические инициализаторы выполняются JVM во время инициализации класса; из-за внутренней синхронизации в JVM, этот механизм гарантирует безопасную публикацию любых объектов, инициализированных таким образом
    [JLS 12.4.2].
    3.5.4 Фактически неизменяемые объекты
    Безопасная публикация является достаточным условием для безопасного доступа других потоков к объектам, которые не будут изменяться после публикации, без использования дополнительной синхронизации.
    Механизмы безопасной публикации гарантируют, что опубликованное состояние объекта станет видимым для всех потоков, получающих доступ к нему, как только станет видна ссылка на него, и если состояние объекта впредь не будет изменяться, это будет достаточным условием для гарантии того, что доступ к нему будет безопасен.
    Объекты, технически не являющиеся неизменяемыми, состояние которых не будет изменяться после публикации, называются фактически неизменяемыми.
    Они не должны следовать строгим определениям неизменяемости из раздела
    3.4
    ; они просто должны рассматриваться программой так, как если бы они были неизменяемыми после их публикации. Использование фактически неизменяемых
    объектов может упростить разработку и повысить производительность за счет снижения расходов на синхронизацию.
    Безопасно опубликованные фактически неизменяемые объекты могут безопасно использоваться любым потоком без дополнительной синхронизации.
    Например, объект
    Date изменяемый
    49
    , но если вы используете его так, как если бы он был неизменяемым, вы можете исключить блокировку, которая в противном случае потребовалась бы при совместном использовании несколькими потоками объекта
    Date
    . Предположим, вы хотите сохранить объект
    Map
    , в котором содержится время последнего входа каждого пользователя: public Map lastLogin =
    Collections.synchronizedMap(new HashMap());
    Если значения типа
    Date не изменяются после их размещения в объекте класса
    Map
    , то синхронизации реализованной в классе synchronizedMap достаточно для безопасной публикации значений типа
    Date
    , и при доступе к ним дополнительная синхронизация не требуется.
    3.5.5 Изменяемые объекты
    Если объект может быть изменен после построения, безопасная публикация обеспечивает только видимость опубликованного состояния. Синхронизация должна использоваться не только для публикации изменяемого объекта, но и при
    49
    Вероятно, была допущена ошибка в дизайне библиотеки классов.
    каждом обращении к объекту, чтобы обеспечить видимость последующих изменений. Для обеспечения безопасного совместного использования изменяемых объектов, они должны быть безопасно опубликованы и быть потокобезопасными или защищёнными блокировкой.
    Требования к публикации объекта зависят от его изменяемости::

    Неизменяемые объекты могут быть опубликованы с использованием любых механизмов;

    Фактически неизменяемые объекты должны быть безопасно опубликованы;
    Изменяемые объекты должны быть безопасно опубликованы и,
    также, должны быть потокобезопасными или защищёнными
    блокировкой.
    3.5.6 Безопасное совместное использование объектов
    Всякий раз, когда вы приобретаете ссылку на объект, вы должны знать, что вам разрешено делать с ним. Вам необходимо захватить блокировку перед его использованием? Можете ли вы изменить его состояние или только прочитать его?
    Множество ошибок параллелизма берёт своё начала с неспособности понять
    «правила взаимодействия» с совместно используемыми объектами. Когда вы публикуете объект, вы должны документировать, как можно получить к нему доступ
    Наиболее полезными политиками для использования и предоставления совместного доступа к объектам в параллельной программе являются:
    Ограничение потока. Объект, ограниченный потоком, принадлежит исключительно одному потоку и ограничен им, и может быть изменен только собственным потоком.
    Совместный доступ “только на чтение”. Объект, с правом совместного доступа “только на чтение”, может быть доступен одновременно нескольким потокам без дополнительной синхронизации, но не может быть изменен ни одним потоком. Совместно используемые объекты с правом “только на чтение” включают неизменяемые и фактически неизменяемые объекты.
    Совместное
    использование
    потокобезопасных
    объектов.
    Потокобезопасный объект выполняет синхронизацию внутри себя, поэтому несколько потоков могут свободно обращаться к нему через его открытые интерфейсы, без необходимости, в дальнейшем, применять синхронизацию.
    Защищённые объекты. К защищённому объекту можно получить доступ только с определенной блокировкой. К защищённым объектам относятся объекты, инкапсулированные в другие потокобезопасные объекты и опубликованные объекты, которые, как известно, защищены определенной блокировкой.

    1   2   3   4   5   6   7   8   9   ...   34


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