4.3.1. Пример: средство отслеживания транспортных средств, использующее делегирование
В качестве более существенного примера делегирования, давайте создадим версию трекера транспортных средств, которая делегирует потокобезопасному классу. Мы сохраняем местоположения на карте, поэтому начнем с поточно-ориентированной реализации карты ConcurrentHashMap
. Мы также сохраняем местоположение, используя неизменный класс Point вместо MutablePoint
, как показано в листинге 4.6.
Листинг 4.6. Класс неизменяемой точки, используемый DelegatingVehicleTracker.
class Point{
public final int x, y;
public Point() {
this.x=0; this.y=0;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point
является потокобезопасным, потому что он неизменен. Неизменяемые значения могут свободно распространяться и публиковаться, поэтому нам больше не нужно копировать местоположения при их возврате.
DelegatingVehicleTracker
в листинге 4.7 не использует явную синхронизацию; весь доступ к состоянию управляется ConcurrentHashMap
, а все ключи и значения карты являются неизменяемыми.
Листинг 4.7. Делегирование безопасности потока в ConcurrentHashMap.
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<String, Point>(points);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations(){
return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable
}
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);
}
}
} 1031 * *
Если бы мы использовали исходный класс MutablePoint
вместо Point, мы бы нарушили инкапсуляцию, позволив getLocations
опубликовать ссылку на изменяемое состояние, которое не является поточно-ориентированным. Обратите внимание, что мы немного изменили поведение класса отслеживания транспортных средств; в то время как версия монитора возвращала снимок местоположений, версия делегирования возвращает неизменяемое, но «живое» представление местоположений транспортного средства. Это означает, что если поток A вызывает getLocations
, а поток B позднее изменяет расположение некоторых точек, эти изменения отражаются в карте, возвращенной в поток A.
4.3.2. Независимые переменные состояния
Мы также можем делегировать безопасность потоков более чем одной базовой переменной состояния, если эти базовые переменные состояния независимы, это означает, что составной класс не налагает никаких инвариантов, включающих несколько переменных состояния.
VisualComponent
в листинге 4.9 - это графический компонент, который позволяет клиентам регистрировать прослушиватели событий мыши и нажатия клавиш. Он поддерживает список зарегистрированных слушателей каждого типа, так что при возникновении события могут быть вызваны соответствующие слушатели. Но нет никакого отношения между набором слушателей мыши и ключевых слушателей; оба они независимы, и поэтому VisualComponent
может делегировать свои обязательства по безопасности потоков двум нижележащим потоки-безопасным спискам.
Листинг 4.9. Передача безопасности потока нескольким переменным базового состояния.
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
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);
}
}
VisualComponent
использует CopyOnWriteArrayList
для хранения каждого списка слушателей; Это поточно-ориентированная реализация List, особенно подходящая для управления списками слушателей (см. Раздел 5.2.3). Каждый список является потокобезопасным, и поскольку нет никаких ограничений, связывающих состояние одного с состоянием другого, VisualComponent
может делегировать свои обязанности по обеспечению безопасности потока базовым объектам mouseListeners
и keyListeners
.
* * 1060 4.3.3. Когда делегирование не проходит
Большинство составных классов не так просты, как VisualComponent
: у них есть инварианты, которые связывают их переменные состояния компонентов. NumberRange
в листинге 4.10 использует два AtomicIntegers
для управления своим состоянием, но накладывает дополнительное ограничение - первое число должно быть меньше или равно второму.
Листинг 4.10. Класс диапазона номеров, который недостаточно защищает свои инварианты. Не делай этого.
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());
}
}
NumberRange
является не поточно-ориентированным ; это не сохраняет инвариант, который ограничивает нижний и верхний. Методы setLower
и setUpper
пытаются соблюдать этот инвариант, но делают это плохо. И setLower
, и setUpper
являются последовательностями «проверяй и действуй», но они не используют достаточную блокировку, чтобы сделать их атомарными. Если диапазон номеров содержит (0, 10), и один поток вызывает setLower(5)
, в то время как другой поток вызывает setUpper(4)
, с некоторым неудачным временем оба пройдут проверки в установщиках, и обе модификации будут применены. В результате диапазон теперь содержит (5, 4) - недопустимое состояние . Так что , в то время как лежащие в основе AtomicInteger являются поточно-ориентированными, составной класс не является . Поскольку базовые переменные состояния lower
и upper
не являются независимыми, NumberRange
не может просто делегировать безопасность потока своим переменным потока, безопасным для потока.
NumberRange
можно сделать поточно-ориентированным, используя блокировку для сохранения его инвариантов, например, защиту нижнего и верхнего с помощью общего замка. Он также должен избегать публикации нижних и верхних страниц, чтобы клиенты не могли подорвать свои инварианты.
Если у класса есть составные действия, как NumberRange
, только делегирование снова не является подходящим подходом для безопасности потока. В этих случаях класс должен обеспечить собственную блокировку, чтобы гарантировать, что составные действия являются атомарными, кроме случаев, когда все составное действие также может быть делегировано базовым переменным состояния.
Если класс составлен из нескольких независимых потоковых переменных состояния и не имеет операций, которые имеют какие-либо недопустимые переходы состояния, он может делегировать безопасность потока базовым переменным состояния.