Синхронизировать по значению, а не объекту

Я хочу сделать что-то подобное в Java

  public void giveMoney(String userId, int money) {
    synchronized (userId) {

        Profile p = fetchProfileFromDB(userId);
        p.setMoney(p.getMoney() + userId);
        saveProfileToDB(p);

    }
   }

Но, конечно, синхронизация по строке неверна. Какой правильный способ сделать что-то подобное?

Ответ 1

Если набор идентификаторов пользователей ограничен, вы можете синхронизировать с интернированной версией String.

Используйте String.intern() (у которого есть несколько недостатков) или что-то вроде Guava Interners, если вам нужно немного больше контролировать интернирование.

Ответ 2

Я думаю, есть несколько вариантов.

Самое простое, что вы можете сопоставить userId с объектом блокировки на карте потокобезопасности. Другие говорили о интернировании, но я не думаю, что это жизнеспособный вариант.

Однако наиболее распространенным вариантом будет синхронизация на p (Профиль). Это удобно, если getProfile() является потокобезопасным, и по его имени я бы предположил, что это возможно.

Ответ 3

В принципе, вы можете синхронизировать любой объект в Java. Это не само по себе "не правильно" для синхронизации на объекте String; это зависит от того, что именно вы делаете.

Но если userId является локальной переменной в методе, то это не сработает. Каждый поток, который выполняет этот метод, имеет свою собственную копию переменной (предположительно, ссылаясь на другой объект String для каждого потока); синхронизация между нитями, конечно, работает только при синхронизации нескольких потоков на одном и том же объекте.

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

class Something {
    private Object lock = new Object();

    public void someMethod() {
        synchronized (lock) {
            // ...
        }
    }
}

Вы также можете использовать явные блокировки из пакета java.util.concurrent.locks, которые могут дать вам больше контроля, если вам это нужно:

class Something {
    private Lock lock = new ReentrantLock();

    public void someMethod() {
        lock.lock();
        try {
            // ...
        } finally {
            lock.unlock();
        }
    }
}

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

Ответ 4

Вы можете использовать прокси-объект для строки.

Object userIdMutex = new Object();

synchronized (userIdMutex) {
    Profile p = getProfile(userId);
    p.setMoney(p.getMoney() + p);
    saveProfile(p);
}

Используйте этот мьютекс при каждом доступе к userId.

Ответ 5

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

Однако, если мы синхронизированы на разных объектах, связь между событиями и данными является сомнительной. Мы должны изучить это, чтобы выяснить. И поскольку в нем участвует GC, к которой не применяется Java-модель памяти, рассуждения могут быть довольно сложными.

Это теоретическое возражение; практически я не думаю, что это вызовет какие-либо проблемы.

Тем не менее, может быть простое, прямое и теоретически правильное решение вашей проблемы. Например Простые блокировки на основе имени Java?

Ответ 6

В соответствии с вашим примером, я предполагаю, что вы хотите получить блокировку для класса профиля, изменить его и затем отпустить блокировку. На мой взгляд, синхронизация не совсем то, что вам нужно. Вам нужен класс, который управляет этими записями, и позволяет блокировать и разблокировать запись, когда в нее нужно внести изменения, а также стиль управления версиями.

Проверьте это: Класс блокировки Java 5

Ответ 7

Как насчет этого:

String userId = ...;
Object userIdLock = new Object();
synchronized (userIdLock) {
    Profile p = getProfile(userId);
    p.setMoney(p.getMoney() + p);
    saveProfile(p);
}

Это простой и, прежде всего, очевидный.