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
це робиться, одне лише делегування знову не є підходящим підходом для безпеки потоку. У цих випадках клас повинен забезпечити власне блокування, щоб гарантувати, що дії сполуки є атомними, за винятком випадків, коли вся дія сполуки може бути делегована змінним базового стану.
Якщо клас складається з декількох незалежних змінних стану, захищених від потоку, і не має операцій, які мають недійсні переходи стану, то він може делегувати безпеку потоку базовим змінним стану.