Я столкнулся с проблемой параллелизма Java, которую, как я думал, решил, но теперь я чувствую, что у моего решения есть проблема.
Проблема:
Внедрите недостающие методы в потоковом безопасном и эффективном образом. Система должна подписаться только на первый запрос на T key
и должна отказаться от подписки, если больше не осталось слушателей для данного ключа.
interface Listener {
void onData();
}
abstract class Publisher<T> {
public void subscribe(T key, Listener l) {
// TODO complete
}
public void unsubscribe(T key, Listener l) {
// TODO complete
}
public void publish(T key) {
// TODO complete
}
public abstract void reallyLongSubscribeRequest(T key);
public abstract void reallyLongUnsubscribeRequest(T key);
}
Мое решение состояло в том, чтобы сохранить ключ и слушателей в ConcurrentHashMap
и использовать исполнитель пула потоков для запуска вызова очень длинных подписных и неподданных методов:
abstract class Publisher<T> {
private final int POOL_SIZE = 5;
private final ConcurrentHashMap<T, List<Listener>> listeners = new ConcurrentHashMap<>();
private final ScheduledExecutorService stpe = Executors.newScheduledThreadPool(POOL_SIZE);
public void subscribe(T key, Listener l) {
if (listeners.containsKey(key)) {
listeners.get(key).add(l);
} else {
final T keyToAdd = key;
final List<Listener> list = new LinkedList<Listener>();
list.add(l);
Runnable r = new Runnable() {
public void run() {
reallyLongSubscribeRequest(keyToAdd);
listeners.putIfAbsent(keyToAdd, list);
}
};
stpe.execute(r);
}
}
public void unsubscribe(T key, Listener l) {
if (listeners.containsKey(key)) {
List<Listener> list = listeners.get(key);
list.remove(l);
if (list.size() == 0) {
final T keyToRemove = key;
Runnable r = new Runnable() {
public void run() {
reallyLongUnsubscribeRequest(keyToRemove);
}
};
stpe.execute(r);
}
}
}
public void publish(T key) {
if (listeners.containsKey(key)) {
final List<Listener> list = listeners.get(key);
for (Listener l : list) {
l.onData();
}
}
}
public abstract void reallyLongSubscribeRequest(T key);
public abstract void reallyLongUnsubscribeRequest(T key);
}
Я теперь обеспокоен тем, что это уже не потокобезопасно, потому что
В подписке активная нить может быть заменена/истечет ее таймлис между входом в ложную ветвь и выполнением Runnable. Если следующий поток выполняет один и тот же вызов (тот же ключ), у нас будет два потока, которые хотят подписаться и записать на карту. putIfAbsent
сохраняет карту согласованной, но очень длинный метод будет вызываться дважды (это плохо, если он изменяет состояние класса).
Как и в # 1, в unssubscribe, что если поток обменивается между вводом истинной ветки вложенного if и выполнения Runnable?
Поэтому мои вопросы
У вас две проблемы:
Ваши методы subscribe
, unsubscribe
и publish
должны быть synchronized
чтобы сделать их потокобезопасными.
У вас должен быть только один поток, чтобы выполнять reallyLong...()
которые ждут в Queue
. Вы отправляете в Queue
сообщение, говорящее о том, чтобы делать то или другое, и это так. Очередь гарантирует, что они происходят один за другим.
У вас также есть ошибка в коде. Вы действительно выполняете reallyLongSubscribeRequest(...)
когда ключ не существует на карте, но вы не удаляете ключ с карты при удалении последнего прослушивателя.
У вас есть пара проблем. Ваши списки не являются потокобезопасными, и вы правы, вы можете запускать запрос несколько раз. ConcurrentHashMap - отличный способ получить параллельный, но потокобезопасный доступ к карте. однако вам необходимо выполнить некоторую синхронизацию "на ключ", чтобы гарантировать правильность операций (un) подписки (не говоря уже о обновлениях списка).
reallyLong...()
пор, пока неreallyLong...()
, чтоreallyLong...()
снижению производительности?