4.3.1. Приклад: Tracker Vehicle Tracker за допомогою делегування
Як більш вагомий приклад делегування давайте побудуємо версію трекера транспортного засобу, яка делегує до безпечного класу. Ми зберігаємо місце в карті, тому ми почнемо з реалізацією карт потокобезпечна, ConcurrentHashMap. Ми також зберігаємо місце розташування, використовуючи незмінний клас Point замість MutablePoint, показаний у Лістингу 4.6.
Лістинг 4.6. Клас Immutable 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. Делегування безпеки нитки одночасною карткою HashMap.
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 пізніше модифікує розташування деяких точок, ці зміни відображаються в карті, поверненій до потоку А.
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використовує a 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це робиться, одне лише делегування знову не є підходящим підходом для безпеки потоку. У цих випадках клас повинен забезпечити власне блокування, щоб гарантувати, що дії сполуки є атомними, за винятком випадків, коли вся дія сполуки може бути делегована змінним базового стану.
Якщо клас складається з декількох незалежних змінних стану, захищених від потоку, і не має операцій, які мають недійсні переходи стану, то він може делегувати безпеку потоку базовим змінним стану.