1
public int saveUserToMap(User user) {
    ReentrantLock lock;
    if(this.userLocks.containsKey(user.getId())) {
        lock = this.userLocks.get(user.getId());
    } else {
        lock = new ReentrantLock();
        ReentrantLock check = this.userLocks.putIfAbsent(user.getId(), lock);
        if(check != null)
            lock = check;
    }

    if(lock.isLocked())
        try {
            lock.wait();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    lock.lock();

    this.users.put(user.getId(), user);
    this.usersByName.put(user.getUsername(), user);
    this.usersByEmail.put(user.getEmail(), user);

    lock.unlock();
    lock.notify();

    return user.getId();
}

Hey, I just want to ask the java developers to check my code if it will be thread-safe and free of Deadlocks as I want to use it in my project. Users, UsersByName and UsersByEmail are ConcurrentHashMap with String, Integer as key and User object as Value. UserLocks is a ConcurrentHashMap with Integer (obviously the user id as key) and a ReentrantLock as value. I want to synchronize the three HashMaps. If someone has a better solution to make a Concurrent map with three keys, it would be nice to post it here. Performance is also important.

Nightloewe
  • 918
  • 1
  • 14
  • 24
  • 1
    _I want to synchronize the three HashMaps_ For what purpose? – OldCurmudgeon Mar 05 '19 at 14:42
  • The three hashmaps have the same value but different keys. I don't want that because of multhreading the three hashmaps have different values. For example, the user with id, username, email is represented in the three hashmaps, now two threads want to update the user, I don't want that the three hashmaps have different values. – Nightloewe Mar 05 '19 at 14:49
  • I'm worried about what would happen if 2 `User` entities with the same id, username, or email but not all 3 get added to your Maps? Is that taken into account somewhere else? – Gray Mar 06 '19 at 21:16

2 Answers2

1

It's thread safe.

If the userId is already in the map, the code gets the lock and use it to synchronize. If not, ConcurrentHashMap provides the syncronization to avoid a race condition to use different locks for the same id.

Afterthat, there is a useless fragment of code, you can get rid off:

if(lock.isLocked())
    try {
        lock.wait();
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

It's not needed because the synchronization is being done using lock.lock(). It's not needed to try to synchronize again using wait() and notify() with the lock object.(Actually, it is not working as you expected, several threads can call lock.isLocked() on the same lock object and get a false until any of the threads calls lock.lock(), but everything between lock and unlock is only being executed by a single Thread at a time).

Also, an usual good practice is to call the lock.unlock() in a finally block.

David SN
  • 3,389
  • 1
  • 17
  • 21
1

I would do it the simple way using synchronized.

class UserMaps {
    private Map<Integer, User> users = new ConcurrentHashMap<>();
    private Map<String, User> usersByName = new ConcurrentHashMap<>();
    private Map<String, User> usersByEmail = new ConcurrentHashMap<>();

    public synchronized int put(User user) {
        users.put(user.getId(), user);
        usersByName.put(user.getUsername(), user);
        usersByEmail.put(user.getEmail(), user);
        return user.getId();
    }
}

This would ensure that all maps are updated consistently so long as all of your getters are also synchronized.

If, however, you want better performance and want to avoid making all of your methods synchronized then use a ReadWriteLock.

class UserMaps {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Map<Integer, User> users = new ConcurrentHashMap<>();
    private Map<String, User> usersByName = new ConcurrentHashMap<>();
    private Map<String, User> usersByEmail = new ConcurrentHashMap<>();

    public int saveUserToMap(User user) {
        lock.writeLock().lock();
        try {
            users.put(user.getId(), user);
            usersByName.put(user.getUsername(), user);
            usersByEmail.put(user.getEmail(), user);
            return user.getId();
        } finally {
            lock.writeLock().unlock();
        }
    }

    public User getById(int id) {
        lock.readLock().lock();
        try {
            return users.get(id);
        } finally {
            lock.readLock().unlock();
        }
    }
}
OldCurmudgeon
  • 64,482
  • 16
  • 119
  • 213
  • Okay, I have another question whether it is useful to use these Maps for user data. I have the user data saved in an mysql database and retrieve them when a user logs in. Every time when the user goes on another page, these user data will be updated. Should I use a map and save changes from my application there while having a database in background or should I directly download it from the db. I want to make the application as performant as possible. – Nightloewe Mar 05 '19 at 16:28
  • Since the collections are CHMs, if you are only looking up in one of them, you would not need the read lock, right? I suspect the whole point of the locks is to keep the 3 collections in sync. – Gray Mar 06 '19 at 21:14
  • If you're synchronizing (either via `synchronize` or a `Lock`) all access to the maps, wouldn't it make more sense to just use a `HashMap`? – Sean Bright Mar 19 '19 at 17:26
  • @Gray Yes but you could do two successive reads and get a bad name/email pair. Source for a GDPR leak right there! – OldCurmudgeon Mar 19 '19 at 18:27
  • Hrm. I don't get it @OldCurmudgeon. If you are doing 2 successive reads then it will be doing 2 successive locks. I was commenting on the `getById(...)` method and saying that the read lock there seems unnecessary with CHM. Right? – Gray Mar 20 '19 at 21:14
  • @Gray But getByName and getByEmail must read-lock too to avoid inconsistencies. – OldCurmudgeon Mar 20 '19 at 21:20
  • Inconsistencies between collections but not inconsistencies in the CHM. Either the id -> user `put()` completed or not. I don't see the reason for the lock. If somehow you were combining data from 2 of the collections then I would understand @OldCurmudgeon. – Gray Mar 20 '19 at 22:35
  • @Gray I was under the impression that between collection inconsistencies was the issue we were trying to resolve. – OldCurmudgeon Mar 22 '19 at 07:25
  • 1
    I agree @OldCurmudgeon. I just don't see 2 operations in the get* methods so I'm not sure what the lock is doing. That's all. No big deal. Cheers dude. – Gray Mar 22 '19 at 12:52