4.3.1. Пример: средство отслеживания транспортных средств с использованием делегирования
В качестве более существенного примера делегирования давайте создадим версию трекера транспортных средств, который делегирует потокобезопасный класс. Мы храним место в карте, поэтому мы начнем с реализацией карт потокобезопасной, ConcurrentHashMap
. Мы также сохраняем местоположение, используя неизменяемый класс Point вместо того MutablePoint
, как показано в листинге 4.6.
Листинг 4.6. Неизменяемый класс 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
для хранения каждого списка слушателей; это поточно-ориентированная реализация List, особенно подходящая для управления списками слушателей (см. Раздел 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) - недопустимое состояние . Таким образом, в то время как базовые AtomicInteger являются потокобезопасными, составной класс - нет . Поскольку базовые переменные состояния lower
иupper
не являются независимыми,NumberRange
не может просто делегировать безопасность потоков своим переменным состояния безопасности потоков.
NumberRange
можно сделать потокобезопасным, используя блокировку для сохранения своих инвариантов, например, защиту нижнего и верхнего уровня с помощью общей блокировки. Он также должен избегать публикации нижнего и верхнего пределов, чтобы клиенты не нарушали его инварианты.
Если у класса есть составные действия, как и в NumberRange
случае с ним, одно лишь делегирование снова не подходит для обеспечения безопасности потоков. В этих случаях класс должен обеспечивать собственную блокировку, чтобы гарантировать, что составные действия являются атомарными, если только составное действие не может быть делегировано базовым переменным состояния.
Если класс состоит из нескольких независимых переменных состояния, безопасных для потоков, и не имеет операций, имеющих недопустимые переходы между состояниями, то он может делегировать безопасность потоков базовым переменным состояния.