Атомно searchKeys () и put () в ConcurrentHashMap - PullRequest
4 голосов
/ 03 марта 2020

Я занимаюсь разработкой веб-сервера на java, который, помимо прочего, должен обеспечивать услугу вызова между парами пользователей. Каждый пользователь может соревноваться только в одном соревновании одновременно. На самом деле я храню объекты Challenge в ConcurrentHashMap<String, Challenge> и использую String, который представляет собой объединение имен пользователей двух игроков в качестве ключей для сопоставлений. Например, если имена пользователей двух игроков - «Микки» и «Гуфи», то ключом объекта Challenge внутри ConcurrentHashMap будет строка:

Mickey:Goofy

При записи нового вызова между двумя пользователями в ConcurrentHashMap я должен проверить, участвуют ли они уже в других вызовах, прежде чем фактически поставить вызов на Карту, другими словами, я должен проверить, есть ли ключ хранится на карте, которая содержит одно из двух имен пользователей игроков, которые хотят начать новый вызов.

Например, при заполненном ConcurrentHashMap<String, Challenge> и запросе вызова для пользователей Mickey и Goofy, я хочу узнать, используя атомный c способ и не блокируя всю карту, если один (или, в конечном итоге, оба) уже участвует / уже участвует в другом зарегистрированном вызове на Карте, и если нет, то поместите новый вызов в карта. Я надеюсь, что все было достаточно ясно.

У кого-нибудь из вас есть предложения?

Заранее спасибо.

Ответы [ 3 ]

6 голосов
/ 03 марта 2020

Использование объединения строк - плохой выбор для составного ключа. Конкатенация строк - это дорогостоящая операция, которая не гарантирует уникальности, поскольку ключ становится неоднозначным, когда одна из строк содержит выбранный вами разделитель.

Конечно, вы можете запретить этот конкретный символ в именах пользователей, но это добавляет дополнительные требования, которые вы должны проверить, в то время как выделенный ключевой объект, содержащий две ссылки, проще и эффективнее. Вы даже можете использовать двухэлементный List<String> как тип ключа add-ho c, поскольку он имеет полезные реализации hashCode и equals.

Но так как вы хотите выполнить поиск для обеих частей в любом случае составного ключа, вы не должны использовать составной ключ в первую очередь. Просто свяжите оба имени пользователя с одним и тем же объектом Challenge. Это все еще не может быть сделано в одной операции atomi c, но для этого не нужно:

final ConcurrentHashMap<String, Challenge> challenges = new ConcurrentHashMap<>();

Challenge startNewChallenge(String user1, String user2) {
    if(user1.equals(user2))
        throw new IllegalArgumentException("same user");

    Challenge c = new Challenge();

    if(challenges.putIfAbsent(user1, c) != null)
        throw new IllegalStateException(user1+" has an ongoing challenge");

    if(challenges.putIfAbsent(user2, c) != null) {
        challenges.remove(user1, c);
        throw new IllegalStateException(user2+" has an ongoing challenge");
    }

    return c;
}

Этот код никогда не будет перезаписывать существующее значение. Если оба putIfAbsent были успешными, то у обоих пользователей определенно не было текущих вызовов, и теперь они оба связаны с одним и тем же новым вызовом.

Когда первый putIfAbsent успешен, но второй не пройден, мы должны удалить первый ассоциация. remove(user1, c) удалит его только тогда, когда пользователь все еще связан с нашей новой задачей. Когда все операции на карте следуют принципу никогда не перезаписывать существующую запись (если не выполнены все предварительные условия), в этом нет необходимости, просто remove(user1) также подойдет. Но использование безопасного варианта здесь не помешает.

Единственная проблема с неатомичностью состоит в том, что две попытки перекрытия с участием одного и того же пользователя могут потерпеть неудачу из-за временно добавленного первого пользователя, когда на самом деле один из них может добиться успеха. Я не считаю это серьезной проблемой; пользователь просто не должен пытаться присоединиться к двум вызовам одновременно.

0 голосов
/ 03 марта 2020

С моей точки зрения, это возможно, но карта должна использовать имена отдельных игроков в качестве ключей, поэтому для обоих игроков мы должны поставить один вызов дважды. Имея это, мы можем ввести дополнительную asyn c проверку того, был ли новый вызов успешно сохранен для обоих игроков.

private boolean put(Map<String, Challenge> challenges, String firstPlayerName,
        String secondPlayerName,
        Challenge newChallenge) {

    if(firstPlayerName.compareTo(secondPlayerName) > 0) {
        String tmp = firstPlayerName;
        firstPlayerName = secondPlayerName;
        secondPlayerName = tmp;
    }

    boolean firstPlayerAccepted = newChallenge == challenges.merge(firstPlayerName, newChallenge,
            (oldValue, newValue) -> oldValue.isInitiated() ? oldValue : newValue);
    boolean secondPlayerAccepted = firstPlayerAccepted
            && newChallenge == challenges.merge(secondPlayerName, newChallenge,
            (oldValue, newValue) -> oldValue.isInitiated() ? oldValue : newValue);

    boolean success = firstPlayerAccepted && secondPlayerAccepted;
    newChallenge.initiate(success);
    if (firstPlayerAccepted) {
        // remove unsuccessful
        challenges.computeIfPresent(firstPlayerName, (s, challenge) -> challenge.isInitiated() ? challenge : null);
        if (secondPlayerAccepted) {
            challenges.computeIfPresent(secondPlayerName, (s, challenge) -> challenge.isInitiated() ? challenge : null);
        }
    }
    return success;
}
class Challenge {

    private final CompletableFuture<Boolean> initiated = new CompletableFuture<>();

    public void initiate(boolean success) {
        initiated.complete(success);
    }

    public boolean isInitiated() {
        try {
            return initiated.get();
        } catch (ExecutionException e) {
            throw new IllegalStateException(e);
        } catch (InterruptedException e) {
            return false;
        }
    }
    enter code here
...
}
0 голосов
/ 03 марта 2020

Вы должны просмотреть свой код.

Вы не можете сделать это за один раз, так как у вас есть два имени для проверки. Даже обычным (итеративным) способом вы в лучшем случае выполняете две операции. Так что в любом случае вам нужно будет сделать как минимум два доступа на карте. Я предлагаю вам использовать вашу реальную карту без объединения строк, так что да, один вызов появится на карте два раза, по одному для каждого участника. Тогда вы сможете легко проверить, занимается ли пользователь.

Если вам нужно знать, с кем он занимается, просто сохраните оба имени в классе Challenge.

Конечно, заблокируйте вашу карту, когда вы ищете обе записи. Функция, которая возвращает логическое значение, сделает всю работу!

...