4.3.1. Пример: Отслеживание транспортных средств с помощью делегирования
В качестве более существенного примера делегирования, позвольте построить версию трекера транспортного средства, который делегирует класс, защищенный потоками. Мы сохраняем местоположения на карте, поэтому мы начинаем с реалистичной реализации Map, ConcurrentHashMap
. Мы также сохраняем местоположение с использованием неизменяемого класса Point вместо MutablePoint
, показанного в листинге 4.6.
Листинг 4.6. Класс Nonutable Point, используемый 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);
}
}
}
Если бы мы использовали исходный класс 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
для хранения каждого списка слушателей; это поточно-безопасная реализация списка, особенно подходящая для управления списками слушателей (см. раздел 5.2.3). Каждый Список является потокобезопасным, и поскольку нет ограничений, связывающих состояние одного с состоянием другого, VisualComponent
может делегировать свои обязанности по обеспечению безопасности потока к базовым объектам mouseListeners
и keyListeners
.
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) - недопустимое состояние. Таким образом, , в то время как базовые AtomicIntegers являются потокобезопасными, составной класс не. Поскольку основные переменные состояния lower
и upper
не являются независимыми, NumberRange
не может просто делегировать безопасность потока в свои переменные состояния, зависящие от потока.
NumberRange
можно сделать потокобезопасным, используя блокировку для поддержания своих инвариантов, например, защиту нижнего и верхнего уровня с помощью общей блокировки. Он также должен избегать публикации в нижней и верхней части, чтобы клиенты не могли подменить свои инварианты.
Если класс имеет составные действия, как это делает NumberRange
, делегирование снова не подходит для обеспечения безопасности потоков. В этих случаях класс должен обеспечить свою собственную блокировку, чтобы гарантировать, что составные действия являются атомарными, если все составное действие также не может быть делегировано основным переменным состояниям.
Если класс состоит из нескольких независимых переменных состояния, не зависящих от потока, и не имеет операций, которые имеют какие-либо недопустимые переходы состояния, тогда он может делегировать безопасность потока для основных переменных состояния.