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

  • 4.2.2 Пример: отслеживание парка автомобилей

  • 4.3 Делегирование

  • 4.3.1 Пример: транспортный трекер с использованием делегирования

  • Листинг 4.8

  • 4.3.3 Когда делегирование не работает

  • 4.3.4 Публикация базовых переменных состояния

  • 4.3.5 Пример: трекер транспортных средств публикующий своё состояние

  • 4.5 Документирование политики синхронизации

  • 4.5.1 Интерпретация расплывчатой документации

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


    Скачать 4.97 Mb.
    НазваниеПри участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
    Анкорjava concurrency
    Дата28.11.2019
    Размер4.97 Mb.
    Формат файлаpdf
    Имя файлаJava concurrency in practice.pdf
    ТипДокументы
    #97401
    страница7 из 34
    1   2   3   4   5   6   7   8   9   10   ...   34
    Листинг 4.3 Защита состояния с помощью приватной блокировки
    Существуют преимущества использования приватного объекта блокировки вместо встроенной блокировки объекта (или любой другой общедоступной блокировки). При создании объекта блокировки приватным, блокировка инкапсулируется таким образом, что клиентский код не может ее получить, в то время как общедоступная блокировка позволяет клиентскому коду участвовать в политике синхронизации - правильно или неправильно. Клиенты, которые получают блокировку другого объекта некорректно, могут вызвать проблемы живучести, и проверка того, что общедоступная блокировка используется правильно, требует проверки всей программы, а не одного класса.
    4.2.2 Пример: отслеживание парка автомобилей
    Класс
    Counter в листинге 4.1 является кратким, но тривиальным примером использования шаблона Java “монитор”. Давайте создадим несколько менее
    54
    Создание шаблона “монитор” было вдохновлено работой Хоара о мониторах (Hoare, 1974), хотя между этим шаблоном и истинным монитором существуют значительные различия. Даже инструкции байт-кода для входа и выхода из синхронизированного блока называются monitorenter и monitorexit
    , а встроенные (внутренние) блокировки Java иногда называют блокировкой монитора или
    монитором.
    тривиальный пример: “трекер автомобилей” для управления парком транспортных средств, таких как такси, полицейские машины или грузовые автомобили. Сначала мы создадим его с помощью шаблона “монитор”, а затем рассмотрим, как ослабить некоторые требования к инкапсуляции, сохранив при этом потокобезопасность.
    Каждое транспортное средство идентифицируется строкой и имеет местоположение, представленное координатами (x, y). Класс
    VehicleTracker инкапсулирует идентификационные данные и местоположения известных транспортных средств, что делает его хорошо подходящим для использования в качестве модели данных в шаблоне модель-представление-контроллер в GUI- приложении, где он может совместно использоваться потоком представления и несколькими потоками, обновляющими данные. Поток представления будет получать имена и местоположения транспортных средств, и отображать их на дисплее:
    Map locations = vehicles.getLocations(); for (String key : locations.keySet()) renderVehicle(key, locations.get(key));
    Точно так же, обновляющие данные потоки будут изменять местоположения транспортных средств, внося данные полученные от устройств GPS, или введённые вручную диспетчером через графический интерфейс: void vehicleMoved(VehicleMovedEvent evt) {
    Point loc = evt.getNewLocation(); vehicles.setLocation(evt.getVehicleId(), loc.x, loc.y);
    }
    Поскольку поток представления и потоки обновлений будут обращаться к модели данных одновременно, она должна быть потокобезопасной. В листинге 4.4 показана реализация трекера транспортных средств с использованием шаблона
    Java “монитор”, который использует класс
    MutablePoint из листинга 4.5 для представления местоположения транспортных средств.
    @ThreadSafe public class MonitorVehicleTracker {
    @GuardedBy("this") private final Map locations; public MonitorVehicleTracker(
    Map locations) { this.locations = deepCopy(locations);
    } public synchronized Map getLocations() { return deepCopy(locations);
    } public synchronized MutablePoint getLocation(String id) {
    MutablePoint loc = locations.get(id); return loc == null ? null : new MutablePoint(loc);

    } public synchronized void setLocation(String id, int x, int y) {
    MutablePoint loc = locations.get(id); if (loc == null) throw new IllegalArgumentException("No such ID: " + id); loc.x = x; loc.y = y;
    } private static Map deepCopy(
    Map m) {
    Map result = new HashMap(); for (String id : m.keySet()) result.put(id, new MutablePoint(m.get(id))); return Collections.unmodifiableMap(result);
    }
    } public class MutablePoint { /
    *
    Листинг 4.5
    *
    / }
    Листинг 4.4 Реализация трекера транспортных средств на основе монитора
    @NotThreadSafe public class MutablePoint { public int x, y; public MutablePoint() { x = 0; y = 0; } public MutablePoint(MutablePoint p) { this.x = p.x; this.y = p.y;
    }
    }
    Листинг 4.5 Класс изменяемой точки похожий на java.awt.Point
    Даже не смотря на то, что класс
    MutablePoint не является потокобезопасным, класс трекера таковым является. Ни объект map
    , ни какие-либо изменяемые точки
    (points), которые он содержит, никогда не публикуются. Когда нам необходимо вернуть местоположение транспортных средств вызывающим объектам, соответствующие значения копируются с использованием копирующего конструктора класса
    MutablePoint
    , либо с помощью метода deepCopy
    , который создает новый объект
    Map
    , значения которого являются копиями ключей и значений старого объекта
    Map
    55
    Эта реализация частично поддерживает потокобезопасность, копируя изменяемые данные перед их возвратом клиенту. Как правило, это не является
    55
    Обратите внимание, что метод deepCopy не может просто обернуть класс
    Map с помощью метода unmodifiableMap
    , поскольку последний защищает от модификации только коллекцию; это не мешает вызывающим объектам модифицировать изменяемые объекты, хранящиеся в ней. По той же причине заполнение класса
    HashMap в методе deepCopy с помощью копирующего конструктора также не сработало бы, потому что были скопированы только ссылки на объекты point
    , а не сами объекты.
    проблемой в плане производительности, но может стать ей, если набор транспортных средств станет очень большим
    56
    . Другим следствием копирования данных при каждом вызове метода getLocation является то, что содержимое возвращаемой коллекции не изменяется, даже если местоположения изменятся в базовой коллекции. Хорошо это или плохо зависит от ваших требований. Это может быть полезно в том случае, если существуют внутренние требования к согласованности в наборе местоположений, и в этом случае возврат согласованного снимка имеет решающее значение, или недостатком, если вызывающему объекту требуется актуальная информация по каждому транспортному средству и, следовательно, возникает необходимость более частого обновления снимка.
    4.3 Делегирование потокобезопасности
    Все, кроме самых тривиальных объектов, являются составными объектами.
    Шаблон Java “монитор” полезен при создании классов с нуля или составлении классов из объектов, которые не являются потокобезопасными. Но что, если компоненты нашего класса уже потокобезопасны? Нужно ли вводить дополнительный уровень для обеспечения потокобезопасности? Ответ... ”это зависит от обстоятельств". В некоторых случаях композит, состоящий из потокобезопасных компонентов, является потокобезопасным (Листинги 4.7 и 4.9), а в других - просто является хорошей основой (листинг 4.10).
    В классе
    CountingFactorizer мы добавили переменную типа
    AtomicLong к объекту без сохранения состояния, и полученный составной объект по-прежнему оставался потокобезопасным. Поскольку состояние
    CountingFactorizer определяется состоянием потокобезопасной переменной типа
    AtomicLong
    , и класс
    CountingFactorizer не накладывает никаких дополнительных ограничений на состояние счетчика, легко увидеть, что класс
    CountingFactorizer является потокобезопасным.
    Мы могли бы сказать, что класс
    CountingFactorizer делегирует ответственность за обеспечение потокобезопасности переменной типа
    AtomicLong
    : класс
    CountingFactorizer потокобезопасен, потому что потокобезопасен класс
    AtomicLong
    57
    4.3.1 Пример: транспортный трекер с использованием
    делегирования
    В качестве более существенного примера делегирования, давайте создадим версию трекера транспортных средств, делегирующую обеспечение безопасности потокобезопасному классу. Мы храним местоположения в объекте
    Map
    , поэтому начнем с реализации потокобезопасной реализации класса
    Map –
    класса
    ConcurrentHashMap
    Мы также храним местоположение с помощью неизменяемого класса
    Point вместо
    MutablePoint
    , как показано в листинге 4.6.
    56
    Поскольку метод deepCopy вызывается из синхронизированного метода, внутренняя блокировка трекера удерживается на протяжении всего времени выполнения длительной операции копирования, что может привести к ухудшению отзывчивости пользовательского интерфейса при отслеживании множества транспортных средств.
    57
    Если поле count не объявлено как final
    , провести анализ класса
    CountingFactorizer на предмет потокобезопасности будет сложнее. Если бы класс
    CountingFactorizer мог изменить поле count
    , с целью назначить ссылку на другой объект
    AtomicLong
    , мы должны были бы гарантировать, что это обновление было бы видимо всем потокам, которые могли бы обратиться к полю count
    , а также гарантировать, чтобы не возникало никаких условий гонки относительно ссылки на поле count. Это еще одна веская причина для того, чтобы использовать final поля там, где это практически целесообразно.

    @Immutable public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y;
    }
    }
    Листинг 4.6 Неизменяемый класс
    Point используемый в классе
    DelegatingVehicleTracker
    Класс Point потокобезопасен, потому что он неизменяемый. Неизменяемые значения могут свободно распространяться и публиковаться, поэтому нам больше не нужно копировать местоположения при их возврате.
    Класс
    DelegatingVehicleTracker в листинге 4.7 не использует явную синхронизацию; весь доступ к состоянию управляется классом
    ConcurrentHashMap
    , а все ключи и значения объекта
    Map неизменяемы.
    @ThreadSafe public class DelegatingVehicleTracker { private final ConcurrentMap locations; private final Map unmodifiableMap; public DelegatingVehicleTracker(Map points) { locations = new ConcurrentHashMap(points); unmodifiableMap = Collections.unmodifiableMap(locations);
    } public Map getLocations() { return unmodifiableMap;
    } public Point getLocation(String id) { return locations.get(id);
    } public void setLocation(String id, int x, int y) { if (locations.replace(id, new Point(x, y)) == null) throw new IllegalArgumentException(
    "invalid vehicle name: " + id);
    }
    }
    Листинг 4.7 Делегирование обеспечения потокобезопасности классу
    ConcurrentHashMap
    Если бы мы использовали исходный класс
    MutablePoint вместо
    Point
    , мы бы нарушили инкапсуляцию, позволив методу getLocations опубликовать ссылку на изменяемое состояние, которое не является потокобезопасным. Обратите внимание, что мы немного изменили поведение класса трекера транспортных
    средств; в то время как версия с монитором возвращала снимок местоположений, делегирующая версия возвращает неизменяемое, но “живое” представление местоположений транспортного средства. Это означает, что если поток A вызывает метод getLocations и поток B позже изменяет местоположение некоторых точек, изменения отражаются в объекте
    Map
    , возвращаемом потоку A. Как мы отмечали ранее, в зависимости от ваших требований это может рассматриваться и как преимущество (более актуальные данные), и как недостаток (потенциально несогласованное представление о парке автомобилей).
    Если требуется неизменяемое представление парка транспортных средств, метод getLocations может вместо него вернуть небольшую копию объекта
    Map переменной locations
    . Поскольку содержимое объекта
    Map является неизменяемым, необходимо скопировать только структуру объекта
    Map
    , а не его содержимое, как показано в листинге 4.8 (в котором возвращается простой объект
    HashMap
    , так как метод getLocations не давал обещание возвращать потокобезопасный объект
    Map
    ). public Map getLocations() { return Collections.unmodifiableMap( new HashMap(locations));
    }
    Листинг 4.8 Возврат статической копии набора местоположений вместо “живой”
    4.3.2 Независимые переменные состояния
    Примеры делегирования до сих пор касались делегирования единственной, потокобезопасной переменной состояния. Мы также можем делегировать потокобезопасность более чем одной базовой (нижележащей) переменной состояния, если базовые переменные состояния независимы; это означает, что составной класс не навязывает никаких инвариантов, включающих несколько переменных состояния.
    Класс
    VisualComponent из листинга 4.9 представляет собой графический компонент, который позволяет клиентам регистрировать слушателей для событий мыши и нажатия клавиш. Он поддерживает список зарегистрированных слушателей каждого типа, поэтому, когда происходит событие, могут быть вызваны соответствующие слушатели. Но нет никакой связи между набором слушателей мыши и слушателей клавиш; оба слушателя являются независимыми, и поэтому класс
    VisualComponent может делегировать свои обязательства по обеспечению потокобезопасности обоим нижележащим потокобезопасным спискам. public class VisualComponent { private final List keyListeners
    = new CopyOnWriteArrayList(); private final List mouseListeners
    = new CopyOnWriteArrayList(); public void addKeyListener(KeyListener listener) { keyListeners.add(listener);
    }
    public void addMouseListener(MouseListener listener) { mouseListeners.add(listener);
    } public void removeKeyListener(KeyListener listener) { keyListeners.remove(listener);
    } public void removeMouseListener(MouseListener listener) { mouseListeners.remove(listener);
    }
    }
    Листинг 4.9 Делегирование потокобезопасности множеству нижележащих переменных состояния
    Класс
    VisualComponent использует метод
    CopyOnWriteArrayList для хранения каждого списка слушателей; это потокобезопасная реализация интерфейса
    List
    , особенно подходящая для управления списками слушателей (см. раздел
    5.2.3
    ).
    Каждый экземпляр интерфейса
    List является потокобезопасным, и поскольку нет никаких ограничений, связывающих состояние одного списка с состоянием другого, класс
    VisualComponent может делегировать свои обязанности по обеспечению потокобезопасности нижележащим объектам mouseListeners и keyListeners
    4.3.3 Когда делегирование не работает
    Большинство составных классов не так просты, как класс
    VisualComponent
    : у них есть инварианты, связанные с переменными состояния компонента. Класс
    NumberRange из листинга 4.10 использует два объекта
    AtomicIntegers для управления своим состоянием, но накладывает дополнительное ограничение - первое число должно быть меньше или равно второму. public class NumberRange {
    // INVARIANT: lower <= upper
    private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) {
    // Warning -- unsafe check-then-act
    if (i > upper.get())
    throw new IllegalArgumentException(
    "can’t set lower to " + i + " > upper"); lower.set(i);
    } public void setUpper(int i) {
    // Warning -- unsafe check-then-act
    if (i < lower.get())
    throw new IllegalArgumentException(
    "can’t set upper to " + i + " < lower"); upper.set(i);

    } public boolean isInRange(int i) { return (i >= lower.get() && i <= upper.get());
    }
    }
    Листинг 4.10 Класс
    NumberRange недостаточно хорошо защищает свои инварианты. Не делайте так.
    Класс
    NumberRange не является потокобезопасным; он не сохраняет инвариант, накладывающий ограничение на объекты lower и upper
    . Методы setLower и setUpper
    пытаются относиться к инварианту с уважением, но делают это плохо.
    Оба метода setLower и setUpper выполняют последовательность действий
    проверить-затем-выполнить
    , но они не используют блокировку достаточную для того, чтобы сделать операции атомарными. Если диапазон номеров удерживается в пределах (0, 10) и один поток вызывает метод setLower
    (5), а другой поток вызывает метод setUpper
    (4), то в некоторый неудачный момент временем оба метода будут проходить проверки в сеттерах, и будут применены обе модификации. В результате, диапазон теперь содержит значения (5, 4) - недопустимое состояние. Поэтому, в то время как базовые объекты
    AtomicIntegers являются потокобезопасными, составной класс таковым не является. Поскольку базовые переменные состояния lower и upper не являются независимыми, класс
    NumberRange не может просто взять и делегировать потокобезопасность своим переменным состояния.
    Класс
    NumberRange можно сделать потокобезопасным, используя блокировку для поддержания сохранности его инвариантов, таких как защита переменных lower и upper с помощью общей блокировки. Также необходимо избегать публикации переменных lower и upper
    , чтобы предотвратить разрушение инвариантов клиентами.
    Если класс имеет составные действия, как в примере с классом
    NumberRange
    , делегирование вновь не является должным подходом для обеспечения потокобезопасности. В таких случаях класс должен предоставлять свою собственную блокировку, чтобы гарантировать, что составные действия являются атомарными, если не всё составное действие может быть делегировано базовым переменным состояния.
    Если класс состоит из нескольких независимых потокобезопасных переменных состояния, и не имеет операций, которые могут приводить к каким-либо недопустимым переходам состояния, тогда он может делегировать потокобезопасность базовым переменных состояния.
    Проблема, которая не позволила классу
    NumberRange быть потокобезопасным, даже если бы его компоненты состояния были потокобезопасными, очень похожа на одно из правил о volatile переменных, описанных в разделе 3.1.4: переменная подходит для объявления как volatile только в том случае, если она не участвует в инвариантах, связанных с другими переменными состояния.

    4.3.4 Публикация базовых переменных состояния
    Когда вы делегируете потокобезопасность базовым переменным состояния объекта, при каких условиях вы сможете опубликовать эти переменные, чтобы другие классы могли также их изменять? Опять же, ответ зависит от того, какие инварианты ваш класс налагает на эти переменные. В то время как базовое поле value класса
    Counter может принимать любое целочисленное значение, класс
    Counter ограничивает его принятием только положительных значений, а операция инкремента ограничивает набор допустимых следующих состояний при заданном текущем состоянии. Если сделать поле value общедоступным, клиенты могут изменить его значение на недопустимое, поэтому публикация приведет к неверному представлению класса. С другой стороны, если переменная представляет текущую температуру или идентификатор последнего пользователя вошедшего в систему, то другой класс может изменить это значение в любое время, вероятно, это не будет нарушать какие-либо инварианты, поэтому публикация этой переменной может быть допустимым решением. (Это может быть не очень хорошей идеей, поскольку публикация изменяемых переменных затруднит разработку в будущем и возможность использования в подклассах, но это не
    обязательно приведет к тому, что класс будет не потокобезопасным.)
    Если переменная состояния является потокобезопасной, она не участвует ни в каких инвариантах, которые ограничивают ее значение и не имеет запрещенных переходов состояний для любой из ее операций, тогда она может быть безопасно опубликована.
    Например, было бы безопасно опубликовать поля mouseListeners или keyListeners из класса
    VisualComponent
    . Поскольку класс
    VisualComponent не накладывает никаких ограничений на допустимые состояния списков слушателей, эти поля могут быть сделаны публичными (public) или опубликованы иным образом без ущерба для потокобезопасности.
    4.3.5 Пример: трекер транспортных средств публикующий
    своё состояние
    Давайте создадим еще одну версию трекера транспортных средств, которая публикует его изменяемое базовое состояние. Нам вновь необходимо немного изменить интерфейс, чтобы приспособить это изменение, на этот раз, используя изменяемые, но потокобезопасные точки.
    @ThreadSafe public class SafePoint {
    @GuardedBy("this") private int x, y; private SafePoint(int[] a) { this(a[0], a[1]); } public SafePoint(SafePoint p) { this(p.get()); } public SafePoint(int x, int y) { this.x = x; this.y = y;

    } public synchronized int[] get() { return new int[] { x, y };
    } public synchronized void set(int x, int y) { this.x = x; this.y = y;
    }
    }
    Листинг 4.11 Потокобезопасный изменяемый класс Point
    Класс
    SafePoint из листинга 4.11 предоставляет геттер, возвращающий оба значения x и y за один раз, путём возврата двухэлементного массива
    58
    . Если бы мы предоставили отдельные геттеры для x и y, то значения могли бы измениться в промежутке времени между моментом получения одной координаты и моментом получения другой, в результате чего, вызывающий код мог увидеть несогласованное значение: местоположение (x, y), где транспортное средство никогда не было. Используя класс
    SafePoint
    , мы можем создать трекер транспортных средств, который публикует базовое изменяемое состояние без ущерба для потокобезопасности, как показано в классе
    PublishingVehicleTracker в листинге 4.12.
    @ThreadSafe public class PublishingVehicleTracker { private final Map locations; private final Map unmodifiableMap; public PublishingVehicleTracker(
    Map locations) { this.locations
    = new ConcurrentHashMap(locations); this.unmodifiableMap
    = Collections.unmodifiableMap(this.locations);
    } public Map getLocations() { return unmodifiableMap;
    } public SafePoint getLocation(String id) { return locations.get(id);
    } public void setLocation(String id, int x, int y) { if (!locations.containsKey(id))
    58
    Приватный конструктор существует, чтобы избежать условия гонки, которое возникло бы, если бы копирующий конструктор был реализован как this(p.x, p.y)
    ; это пример идиомы захвата приватного конструктора (Bloch and Gafter, 2005).
    throw new IllegalArgumentException( "invalid vehicle name: " + id); locations.get(id).set(x, y);
    }
    }
    Листинг 4.12. Трекер транспортных средств безопасно публикующий базовое состояние
    Класс
    PublishingVehicleTracker наследует потокобезопасность путём делегирования базовому классу
    ConcurrentHashMap
    , но на этот раз содержимым класса
    Map являются потокобезопасные изменяемые точки, а не неизменяемые.
    Метод getLocations возвращает неизменяемую копию базового объекта
    Map
    Вызывающие не могут добавлять или удалять транспортные средства, но могут изменять местоположение одного из транспортных средств, изменяя значения объектов
    SafePoint в возвращаемом объекте
    Map
    . Вновь, “живая” природа класса
    Map может быть как преимуществом, так и недостатком, в зависимости от ваших требований. Класс
    PublishingVehicleTracker является потокобезопасным, но это было бы не так, если бы он налагал какие-либо дополнительные ограничения на допустимые значения местоположений транспортных средств. Если возникает необходимость в наложении “вето” на изменение местоположения транспортных средств или принятии мер при изменении местоположения, подход, принятый в классе
    PublishingVehicleTracker не подойдёт.
    4.4 Добавление функциональности в существующие
    потокобезопасные классы
    Библиотека классов Java содержит множество полезных классов - “строительных блоков”. Повторное использование существующих классов часто предпочтительнее создания новых: повторное использование может снизить сложность разработки, риски разработки (поскольку существующие компоненты уже протестированы) и затраты на сопровождение. Иногда потокобезопасный класс, поддерживающий все операции, которые нам необходимы, уже существует, но часто лучшее, что мы можем найти, это класс, который поддерживает почти все необходимые операции, и нам остаётся только добавить к нему новую операцию, не нарушив его потокобезопасность.
    В качестве примера предположим, что нам нужна потокобезопасная реализация интерфейса
    List с атомарной операцией положить-если-отсутствует (put-if-
    absent). Синхронизированные реализации интерфейса
    List почти полностью выполняют эту работу, так как они предоставляют методы contains и add
    , из которых мы можем построить операцию положить-если-отсутствует.
    Концепция положить-если-отсутствует достаточно проста - проверьте, есть ли элемент в коллекции, прежде чем добавлять его, и не добавляйте, если он уже в ней. (Сейчас должен прекратиться предупреждающий звон колокольчиков операции проверить-затем-выполнить.) Требование, чтобы класс был потокобезопасным, неявно добавляет еще одно требование - чтобы такие операции, как положить-если-отсутствует, были атомарными. Любое разумное толкование предполагает, что если взять объект типа
    List
    , который не содержит объект X, и добавить объект X дважды с помощью операции положить-если-
    отсутствует, результирующая коллекция будет содержать только одну копию объекта X. Но, если операция положить-если-отсутствует, была не атомарной, в некоторый неудачный момент времени два потока могли увидеть, что объект X
    отсутствует в списке и оба могли добавить объект X, в результате в списке будет находиться две копии объекта X.
    Самый безопасный способ добавить новую атомарную операцию - изменить исходный класс для поддержки нужной операции, но это не всегда возможно, так как у вас может не быть доступа к исходному коду или вы не можете свободно изменять его. Если есть возможность изменить исходный класс, необходимо понять реализацию политики синхронизации, чтобы можно было изменить его в канве исходного дизайна. Добавление нового метода в класс означает, что весь код, который реализует политику синхронизации, для этого класса, все ещё находится в одном исходном файле, что облегчает понимание и сопровождение.
    Другой подход заключается в расширении класса, принимая допущение, что он был разработан для расширения. Класс
    BetterVector в листинге 4.13 расширяет класс
    Vector
    , чтобы добавить метод putIfAbsent
    . Расширение класса
    Vector достаточно простое, но не все классы предоставляют достаточный доступ к своему состоянию подклассам, чтобы позволять использование такого подхода.
    Расширение более хрупко, чем добавление кода непосредственно в класс, поскольку реализация политики синхронизации теперь распространяется на несколько отдельно поддерживаемых файлов с исходным кодом. Если базовый класс изменит свою политику синхронизации, выбрав другой тип блокировки для защиты переменных состояния, подкласс незаметно и тихо сломается, так как он больше не сможет использовать правильную блокировку для управления параллельным доступом к состоянию базового класса. (Политика синхронизации класса
    Vector зафиксирована в его спецификации, поэтому класс
    BetterVector не пострадает от этой проблемы.)
    @ThreadSafe public class BetterVector extends Vector { public synchronized boolean putIfAbsent(E x) { boolean absent = !contains(x); if (absent) add(x); return absent;
    }
    }
    Листинг 4.13 Расширение класса
    Vector для добавления метода putIfAbsent
    4.4.1 Блокировка на стороне клиента
    Для класса
    ArrayList
    , обернутого методом
    Collections.synchronizedList
    , ни один из подходов - добавление метода к исходному классу или расширение класса - не работает, потому что клиентский код даже не знает что класс объекта
    List
    , возвращается синхронизирующим фабричным методом-оберткой. Третья стратегия заключается в расширении функциональности класса без расширения самого класса, путем размещения кода расширения во “вспомогательном” (helper) классе.
    В листинге 4.14 показана неудачная попытка создать вспомогательный класс с атомарной операцией
    положить-если-отсутствует для работы с потокобезопасным объектом
    List
    @NotThreadSafe public class ListHelper {
    public List list =
    Collections.synchronizedList(new ArrayList()); public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent;
    }
    }
    Листинг 4.14 Непотокобезопасная попытка реализации операции положить-если-отсутствует
    Почему бы это не сработало? Ведь метод putIfAbsent помечен как synchronized
    , верно? Проблема в том, что он синхронизируется на неверной блокировке. Какую бы блокировку объект типа
    List не использовал для защиты своего состояния, она не является собственной блокировкой класса
    ListHelper.
    Класс
    ListHelper предоставляет только иллюзию синхронизации; различные операции списка, не смотря на то, что все синхронизированы, используют разные блокировки, что означает, что метод putIfAbsent
    не является атомарным по отношению к другим операциям в объекте
    List
    . Таким образом, нет никакой гарантии, что другой поток не изменит список во время выполнения метода putIfAbsent
    Чтобы этот подход работал, мы должны использовать ту же блокировку, что и объект
    List
    , с помощью блокировки на стороне клиента (client-side locking) или
    внешней блокировки (external locking). Блокировка на стороне клиента влечёт за собой защиту клиентского кода, который использует некоторый объект X с использованием блокировки X для защиты своего собственного состояния. Чтобы использовать блокировку на стороне клиента необходимо знать, какую блокировку использует объект X.
    Документация на класс
    Vector и классы синхронизирующих обёрток
    (synchronized wrapper) утверждает, хотя и косвенно, что они поддерживают блокировку на стороне клиента, используя встроенную блокировку для класса
    Vector или коллекцию классов-обёрток (не обёрнутую коллекцию). В листинге
    4.15 показана операция putIfAbsent в потокобезопасном объекте
    List
    , корректно использующем клиентскую блокировку.
    @ThreadSafe public class ListHelper { public List list =
    Collections.synchronizedList(new ArrayList()); public boolean putIfAbsent(E x) {
    synchronized (list) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent;
    }
    }
    }
    Листинг 4.15 Реализация операции положить-если-отсутствует с блокировкой на стороне клиента

    Если расширение класса для добавления другой атомарной операции является хрупким, так как он распределяет код блокировки класса по нескольким классам в иерархии объектов, блокировка на стороне клиента является еще более хрупкой, поскольку она влечет за собой размещение кода блокировки класса C в классы, которые полностью не связаны с классом C. Проявляйте осторожность при использовании блокировки на стороне клиента для классов, которые не фиксируют свою стратегию блокировки.
    Блокировка на стороне клиента имеет много общего с расширением класса - они оба сочетают поведение производного класса с реализацией базового класса.
    Подобно тому, как расширение нарушает инкапсуляцию реализации [EJ Item 14], блокировка на стороне клиента нарушает инкапсуляцию политики синхронизации.
    4.4.2 Композиция
    Существует менее хрупкая альтернатива для добавления атомарной операции в существующий класс: композиция. Класс
    ImprovedList из листинга 4.16 реализует операции интерфейса
    List
    , путём делегирования их базовому экземпляру интерфейса
    List и добавляет атомарный метод положить-если-отсутствует.
    (
    Подобно фабричному методу
    Collections.synchronizedList и другим классам- обёрткам коллекций, класс
    ImprovedList предполагает, что однажды передав список в его конструктор, клиент не будет использовать базовый список напрямую, но получая к нему доступ только через класс
    ImprovedList.
    )
    @ThreadSafe public class ImprovedList implements List { private final List list; public ImprovedList(List list) { this.list = list; } public synchronized boolean putIfAbsent(T x) { boolean contains = list.contains(x); if (contains) list.add(x); return !contains;
    } public synchronized void clear() { list.clear(); }
    // ... similarly delegate other List methods
    }
    Листинг 4.16 Реализация операции положить-если- отсутствует с использованием композиции
    Класс
    ImprovedList добавляет дополнительный уровень блокировки с использованием собственной встроенной блокировки.
    Не имеет значения, является ли базовый класс
    List потокобезопасным, поскольку он обеспечивает собственную согласованную блокировку, обеспечивающую безопасность потоков, даже если класс
    List не является потокобезопасным или его реализация блокировки изменится. Хотя дополнительный уровень синхронизации может привести к небольшому снижению производительности
    59
    , реализация подхода как в
    59
    Штраф будет небольшим, потому что синхронизация в базовом классе
    List гарантированно не будет затронута и, следовательно, будет быстрой; см. главу 11.

    ImprovedList менее хрупкая, чем попытка имитировать стратегию блокировки другого объекта. Фактически, мы использовали шаблон Java монитор для инкапсуляции существующего объекта
    List
    , и это гарантирует потокобезопасность, пока наш класс содержит единственную выдающуюся ссылку на базовый объект
    List
    4.5 Документирование политики синхронизации
    Документация является одним из самых мощных (и, к сожалению, наиболее сильно недоиспользуемых) инструментов для управления потокобезопасностью.
    Пользователи обращаются к документации, чтобы узнать, является ли класс потокобезопасным, а сопровождающие смотрят на документацию, чтобы понять стратегию реализации, чтобы они могли поддерживать его без непреднамеренной компрометации безопасности. К сожалению, обе группы клиентов обычно находят в документации меньше информации, чем им хотелось бы.
    Документируйте гарантии потокобезопасности класса для его клиентов; документируйте политику синхронизации для сопровождающих его.
    Каждое использование операторов synchronized
    , volatile или любого потокобезопасного класса отражается в политике синхронизации, определяющей стратегию обеспечения целостности данных перед лицом параллельного доступа.
    Эта политика является элементом дизайна вашей программы и должна быть задокументирована. Конечно, лучшее время для документирования проектных решений - этап проектирования. Недели или месяцы спустя, детали могут быть размыты - так что запишите их, прежде чем забыть.
    Для создания политики синхронизации требуется принять ряд решений: какие переменные сделать volatile
    , какие переменные защитить с помощью блокировок, какие блокировки защищают какие переменные, какие переменные должны быть неизменяемыми или должны быть ограничены потоком, какие операции должны быть атомарными и т.д. Некоторые из них являются подробными сведениями о реализации и должны быть задокументированы для будущих сопровождающих, но некоторые из них влияют на публично наблюдаемое поведение блокировки вашего класса и должны быть задокументированы как часть его спецификации.
    По крайней мере, задокументируйте гарантии потокобезопасности, сделанные классом. Это потокобезопасно? Делает ли он обратные вызовы, удерживая блокировку? Существуют ли какие-либо специфичные блокировки, влияющие на его поведение? Не заставляйте клиентов делать рискованные предположения. Если вы не хотите поддерживать блокировку на стороне клиента, это нормально, но так и скажите. Если вы хотите, чтобы клиенты могли создавать новые атомарные операции в вашем классе, как мы это делали в разделе 4.4, вам нужно задокументировать, какие блокировки они должны захватить, чтобы сделать это безопасно. Если вы используете блокировки для защиты состояния, задокументируйте это для будущих сопровождающих, потому что это так просто - аннотация
    @GuardedBy выполнит за вас этот трюк. Если вы используете более тонкие средства для обеспечения потокобезопасности, документируйте их, потому что они могут быть не очевидны для сопровождающих.
    Текущее состояние дел в документации по потокобезопасности, даже в библиотеке классов платформы, не обнадеживает. Сколько раз вы смотрели в

    Javadoc на описание класса и задавались вопросом, является ли он потокобезопасным?
    60
    Так или иначе, большинство классов не предлагают никаких подсказок. Многие официальные спецификации Java-технологий, такие как сервлеты и JDBC, удручающе относятся к документированию своих обещаний и требований к безопасности потоков.
    Хотя благоразумие наводит на мысль, что мы не предполагаем поведения, не входящего в спецификацию, у нас есть работа, которая должна быть выполнена, и мы часто сталкиваемся с последствиями выбора плохих допущений. Должны ли мы предположить, что объект является потокобезопасным, потому как кажется, что он таковым должен быть? Должны ли мы предположить, что доступ к объекту можно сделать потокобезопасным, захватывая его блокировку? (Этот рискованный метод работает только в том случае, если мы контролируем весь код, который обращается к этому объекту; в противном случае, он нам дает лишь иллюзию потокобезопасности.) Ни один из вариантов не является удовлетворительным.
    Усугубляя проблему, наша интуиция часто может ошибаться в том, какие классы “вероятно потокобезопасны", а какие нет. Например, класс java.text.SimpleDateFormat не является потокобезопасным, но в Javadoc забыли упомянуть об этом вплоть до JDK 1.4. То, что этот конкретный класс не является потокобезопасным, вызвало удивление многих разработчиков. Сколько программ ошибочно создают совместно используемый экземпляр объекта, не являющегося потокобезопасным, и используют его из нескольких потоков, не подозревая, что это может привести к ошибочным результатам при большой нагрузке?
    Проблему с классом
    SimpleDateFormat можно обойти, если не считать класс потокобезопасным, если об этом не сказано прямо. С другой стороны, невозможно создать приложение на основе сервлета, не делая довольно сомнительных предположений о потокобезопасности объектов, предоставляемых контейнером, подобных
    HttpSession
    . Не заставляйте своих клиентов или коллег делать догадки, подобные этим.
    4.5.1 Интерпретация расплывчатой документации
    Многие спецификации технологий Java молчат или, по меньшей мере, неудовлетворительно рассказывают о гарантиях потокобезопасности и требованиях к таким интерфейсам, как
    ServletContext
    ,
    HttpSession или
    DataSource
    61
    Поскольку эти интерфейсы реализуются поставщиком контейнера или базы данных, часто невозможно просмотреть код, чтобы увидеть, что он делает. Кроме того, вы не хотите полагаться на детали реализации одного конкретного драйвера
    JDBC - вы хотите иметь совместимость со стандартом, так чтобы ваш код работал должным образом с любым драйвером JDBC. Но слова "поток” и "параллельный" вообще не появляются в спецификации JDBC и появляются удручающе редко в спецификации сервлета. Так чем вы займётесь?
    Вам придется гадать. Один из способов улучшить качество вашей догадки - интерпретировать спецификацию с точки зрения того, кто ее реализует (например, поставщик контейнера или базы данных), в отличие от того, кто просто ее использует. Сервлеты всегда вызываются из потока, управляемого контейнером, и можно с уверенностью предположить, что если есть более одного такого потока, контейнер знает об этом. Контейнер сервлета предоставляет определенные
    60
    Если вы никогда не задавались этим вопросом, мы восхищаемся вашим оптимизмом.
    61
    Мы находим особенно расстраивающим то, что эти упущения сохраняются, несмотря на многочисленные пересмотры спецификаций.
    объекты, которые обеспечивают обслуживание множества сервлетов, такие как
    HttpSession или
    ServletContext
    . Таким образом, контейнер сервлета должен рассчитывать, что эти объекты будут доступны одновременно, так как он создал несколько потоков и вызвал у них методы, подобные
    Servlet.service
    , что вполне ожидаемо при доступе к ServletContext.
    Поскольку невозможно представить однопоточный контекст, в котором эти объекты были бы полезны, следует предположить, что они были созданы потокобезопасными, хотя спецификация явно не требует этого. Кроме того, если им требуется блокировка на стороне клиента, на какой блокировке должен синхронизироваться код клиента? Документация ничего не говорит об этом, и кажется абсурдным, что приходится гадать. Это “разумное предположение” далее подкрепляется примерами в спецификации и официальными руководствами, которые показывают, как обращаться к классам
    ServletContext или
    HttpSession и не использовать какую-либо клиентскую синхронизацию.
    С другой стороны, объекты, помещенные в экземпляры
    ServletContext или
    HttpSession с помощью метода setAttribute
    , принадлежат веб-приложению, а не контейнеру сервлета. Спецификация сервлета не предполагает какого-либо механизма для координации параллельного доступа к совместно используемым атрибутам. Поэтому атрибуты, хранящиеся в контейнере от имени веб-приложения, должны быть потокобезопасными или эффективно неизменяемыми. Если бы контейнер хранил все атрибуты от имени веб-приложения, другим вариантом была бы гарантия, что они будут согласованно защищены блокировкой при доступе из кода приложения сервлета. Но поскольку контейнеру может потребоваться сериализовать объекты, содержащиеся в
    HttpSession для репликации или пассивации, а контейнер сервлета не может о реализуемом объектами протоколе блокировки, следует сделать их потокобезопасными.
    Можно сделать аналогичный вывод об интерфейсе JDBC
    DataSource
    , который представляет собой пул повторно используемых соединений с базой данных.
    Интерфейс
    DataSource предоставляет приложению службу, и это не имеет большого смысла в контексте однопоточного приложения. Трудно представить себе вариант использования, который не включает вызов метода getConnection из нескольких потоков. И, как и в случае с сервлетами, примеры в спецификации
    JDBC не предполагают потребности в любой клиентской блокировке во множестве примеров кода, использующих объекты
    DataSource
    . Так что, хотя спецификация не утверждает, что источник данных является потокобезопасным и не требует, чтобы поставщик контейнера обеспечивал потокобезопасную реализацию, к тому же, учитывая аргумент “было бы нелепо, если бы не”, у нас нет выбора, кроме как предположить, что вызов
    DataSource getConnection не требует дополнительной блокировки на стороне клиента.
    С другой стороны, мы не будем приводить те же аргументы в отношении объектов JDBC
    Connection
    , порождаемых объектами
    DataSource
    , поскольку они не обязательно предназначены для совместного использования другими активностями (activity) до тех пор, пока не будут возвращены в пул соединений.
    Поэтому, если активность, которая получает объект JDBC
    Connection
    , охватывает несколько потоков, она должна нести ответственность за обеспечение надлежащего контроля доступа к соединению посредством синхронизации. (В большинстве приложений активности, использующие объекты JDBC
    Connection
    , реализованы таким образом, чтобы в любом случае ограничивать подключение определенным потоком.)

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


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