Description below the code...
// Singleton
public static final Map<String, Account> SHARED_ACCOUNT_HASHMAP =
Collections.synchronizedMap(new HashMap<>());
public init(String[] credentials) {
Account account = null;
String uniqueID = uniqueAccountIdentifier(credentials);
if (SHARED_ACCOUNT_HASHMAP.containsKey(uniqueID)) {
account = SHARED_ACCOUNT_HASHMAP.get(uniqueID);
log("...retrieved Shared Account object: %s", uniqueID);
}
// create the Account object (if necessary)
if (account == null) {
account = new Account(credentials);
// Store it in the SHARED_ACCOUNT_HASHMAP
SHARED_ACCOUNT_HASHMAP.put(uniqueID, account);
log("...created Account object: %s",uniqueID);
}
}
What I want to achieve
There are multiple Threads accessing this Singleton HashMap
The goal of this HashMap is to only allow the creation of ONE Account per uniqueID
The account later can be retrieved by various threads for Account operations
Each Thread has this init() method and runs it once.
So the first Thread that cannot find an existing uniqueID Account, creates a new one and places it in the HashMap. The next Thread finds that for the same uniqueID, there is an Account object already - so retrieves it for its own use later
My problem...
How can I get the other Threads (second, third, etc.) to wait while the first Thread is inserting a new Account object?
to phrase it another way, there should never be 2 threads ever that receive a value of null when reading the HashMap for the same uniqueID key. The first thread may receive a value of null, but the second should retrieve the Account object that the first placed there.
According to the docs for synchronizedMap()
Returns a synchronized (thread-safe) map backed by the specified map. In order to guarantee serial access, it is critical that all access to the backing map is accomplished through the returned map.
It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views
In other words you still need to have synchronized access to SHARED_ACCOUNT_HASHMAP:
public init(String[] credentials) {
Account account = null;
String uniqueID = uniqueAccountIdentifier(credentials);
synchronized (SHARED_ACCOUNT_HASHMAP) {
if (SHARED_ACCOUNT_HASHMAP.containsKey(uniqueID)) {
account = SHARED_ACCOUNT_HASHMAP.get(uniqueID);
log("...retrieved Shared Account object: %s", uniqueID);
}
// create the Account object (if necessary)
if (account == null) {
account = new Account(credentials);
// Store it in the SHARED_ACCOUNT_HASHMAP
SHARED_ACCOUNT_HASHMAP.put(uniqueID, account);
log("...created Account object: %s",uniqueID);
}
}
}
Consider using ReadWriteLock if you have multiple readers/writers (see ReadWriteLock example).
Generally the ConcurrentHashMap performs better than the sinchronized hash map you are using.
In the following code I can feel smell of race condition check-then-act as you are trying to perform two operations on the synchronised map (containsKey and get):
if (SHARED_ACCOUNT_HASHMAP.containsKey(uniqueID)) {
account = SHARED_ACCOUNT_HASHMAP.get(uniqueID);
log("...retrieved Shared Account object: %s", uniqueID);
}
So to avoid race condition you need to synchronize over this map as:
synchronized (synchronizedMap) {
if (SHARED_ACCOUNT_HASHMAP.containsKey(uniqueID)) {
account = SHARED_ACCOUNT_HASHMAP.get(uniqueID);
log("...retrieved Shared Account object: %s", uniqueID);
}
// rest of the code.
}
Actually the synchronizedMap can protect itself against internal race conditions that could corrupt the map data but for external conditions (like above) you need to take care of that. If you feel you are using synchronized block at many places you can also think of using a regular map along with synchronized blocks. You will find this question also useful.
Related
I am developing a web server in java which, among other things, is supposed to implement a challenge service between couples of users.
Each user can compete in only one challenge at a time.
Actually I am storing the "Challenge" objects in a ConcurrentHashMap<String, Challenge> and I am using a String that is the union of the two players usernames as keys for mappings.
For example, if the usernames of the two players are "Mickey" and "Goofy" then the key of the Challenge object inside the ConcurrentHashMap will be the string:
Mickey:Goofy
When recording a new challenge between two users in the ConcurrentHashMap, i have to check if they are already engaged in others challenges before actually putting the challenge in the Map, in other words, i have to check if there is a key stored in the Map that contains one of the two usernames of the players which want to start the new challenge.
For example, given a filled ConcurrentHashMap<String, Challenge> and a challenge request for the users Mickey and Goofy, i want to know in an atomic way and without locking whole map, if one (or eventually both) of them is/are already engaged in other registered challenge within the Map and if not, then put the new Challenge in the Map.
I hope to have been clear enough.
Do any of you have a suggestion?
Thanks in advance.
Using string concatenation is a bad choice for a compound key. String concatenation is an expensive operation and it doesn’t guaranty uniqueness, as the key becomes ambiguous when one of the strings contains the separator of your choice.
Of course, you can forbid that particular character in user names, but this adds additional requirements you have to check, whereas a dedicated key object holding two references is simpler and more efficient. You may even use a two element List<String> as an add-hoc key type, as it has useful hashCode and equals implementations.
But since you want to perform lookups for both parts of the compound key anyway, you should not use a compound key in the first place. Just associate both user names with the same Challenge object. This still can’t be done in a single atomic operation, but it doesn’t need to:
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;
}
This code will never overwrite an existing value. If both putIfAbsent were successful, both user definitely had no ongoing challenge and are now both associated with the same new challenge.
When the first putIfAbsent succeeded but the second fails, we have to remove the first association. remove(user1, c) will only remove it when the user still is associated with our new challenge. When all operations on the map follow the principle to never overwrite an existing entry (unless all prerequisites are met), this is not necessary, a plain remove(user1) would do as well. But it doesn’t hurt to use the safe variant here.
The only issue with the non-atomicity is that two overlapping attempts involving the same user could both fail, due to the temporarily added first user, when actually one of them could succeed. I do not consider that a significant problem; the user simply shouldn’t attempt to join two challenges at the same time.
You must review your code.
You cannot do this in one time as you have two names to check.
Even in a conventional (iterating) way you would have in the best case two operation.
So anyway you will need to do at least two access on the map.
I suggest you to use your actual map without the concatenation of strings, so yes, one Challenge will appear two time in the map, one for each participant. Then you will be able to check easily if a user is engaged.
If you need to know with whom he is engaged, simply store the both names in the Challenge class.
Of course lock your map when you are looking for both entries. A function who return a Boolean will do the job !
From my perspective it's possible, but the map has to use individual player names as keys, so for both players we have to put one challenge twice.
Having this, we can introduce additional async checking whether the new challenge was successful stored for the both players.
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
...
}
private Map<CustomerKey, Customer> customerMap = new ConcurrentHashMap<CustomerKey, Customer>();
public Customer getCustomer(CustomerKey customerKey)
{
Customer customer = customerMap.get(customerKey);
if(null == customer)
{
synchronized(this)
{
customer = customerMap.get(customerKey); // Added line
if(null == customer)
{
customer = new Customer();
customerMap.put(customerKey, customer); // Added line
}
}
}
return customer;
}
This is how we usually do object level locking.
In this example, object level locking is applied irrespective of the value of the customerKey object. So even for different customerKey objects, the particular block will be synchronized. I don't want this behavior.
Instead of "this" variable, if I acquire lock on customerKey object like below,
synchronized(customerKey)
Multiple threads can be passing different customerKey objects but with same value which means they are meaningfully equal (customerKeyThread1.equals(customerKeyThread2)) but not same objects (customerKeyThread1 != customerKeyThread2)
So locking at customerKey object is also not a valid solution.
So I need some logic which provides synchronization for a set of code not only for same object but also for meaningfully equal objects. How can I achieve the same?
Well you made it simpler in such a case (it seems to me), just use ConcurrentHashMap.computeIfAbsent as it is documented with:
the current (existing or computed) value associated with the specified key, or null if the computed value is null.
It also says that:
The entire method invocation is performed atomically....
So you could do:
public Customer getCustomer(CustomerKey customerKey) {
return customerMap.computeIfAbsent(customerKey, x-> new Customer());
}
I have a cache class which is based on ConcurrentHashMap. This cache is used to store results I get from a relatively slow reference data service.
One problem of this is that, when multiple threads try to get a key that does not exist, both thread will go and fetch the same key from the reference data service, resulting in two reference data calls.
I am thinking to improve the implementation of the cache so that only one of the threads query the reference data service.
Is there any standard implementation for this?
Here is sample code which stores the unique keys in a List<> keyLocks and if a object with the equivalent value is passed it will return the same key for it, and then a synchroized block on the key
private final List<Object> keyLocks = new ArrayList<>(); // field in Cache
public Object get(Object key){
Object lock;
synchronized (keyLocks) {
if (!keyLocks.contains(key)) {
keyLocks.add(key);
lock = key;
} else {
lock = keyLocks.get(keyLocks.indexOf(key));
}
}
synchronized (lock) {
if(innerCache.containsKey(key)){
return cache.get(key);
}else{
Object result = dataService.get(key);
innerCache.put(key,result);
return result;
}
}
}
I'm trying to create a method with a ConcurrentHashMap with the following behavior.
Read no lock
Write lock
prior to writing,
read to see if record exist,
if it still doesn't exist, save to database and add record to map.
if record exist from previous write, just return record.
My thoughts.
private Object lock1 = new Object();
private ConcurrentHashMap<String, Object> productMap;
private Object getProductMap(String name) {
if (productMap.isEmpty()) {
productMap = new ConcurrentHashMap<>();
}
if (productMap.containsKey(name)) {
return productMap.get(name);
}
synchronized (lock1) {
if (productMap.containsKey(name)) {
return productMap.get(name);
} else {
Product product = new Product(name);
session.save(product);
productMap.putIfAbsent(name, product);
}
}
}
Could someone help me to understand if this is a correct approach?
There are several bugs here.
If productMap isn't guaranteed to be initialized, you will get an NPE in your first statement to this method.
The method isn't guaranteed to return anything if the map is empty.
The method doesn't return on all paths.
The method is both poorly named and unnecessary; you're trying to emulate putIfAbsent which half accomplishes your goal.
You also don't need to do any synchronization; ConcurrentHashMap is thread safe for your purposes.
If I were to rewrite this, I'd do a few things differently:
Eagerly instantiate the ConcurrentHashMap
Bind it to ConcurrentMap instead of the concrete class (so ConcurrentMap<String, Product> productMap = new ConcurrentHashMap<>();)
Rename the method to putIfMissing and delegate to putIfAbsent, with some logic to return the same record I want to add if the result is null. The above absolutely depends on Product having a well-defined equals and hashCode method, such that new Product(name) will produce objects with the same values for equals and hashCode if provided the same name.
Use an Optional to avoid any NPEs with the result of putIfAbsent, and to provide easier to digest code.
A snippet of the above:
public Product putIfMissing(String key) {
Product product = new Product(key);
Optional<Product> result =
Optional.ofNullable(productMap.putIfAbsent(key, product));
session.save(result.orElse(product));
return result.orElse(product);
}
If I look at ConcurrentHashMap at java, and specifically the putIfAbsent method, a typical usage of this method would be:
ConcurrentMap<String,Person> map = new ConcurrentHashMap<>();
map.putIfAbsent("John",new Person("John"));
The problem is that the Person object is always initialized.
Is there some helper collection (maybe some java framework providing this)
that will give me similar behavior of ConcurrentHashMap, and that will work with a functor or any other mean to construct the value object,
and the construction code (i.e - functor.execute() ) will be called only if the map does not contain a value for the given key?
The only way to do this is to use locking. You can minimise the impact of this by using checking first.
if(!map.containsKey("John"))
synchronized(map) {
if(!map.containsKey("John"))
map.put("John", new Person("John"));
}
The reson you need locking is that you need to hold the map while you create the Person to prevent other threads trying to add the same object at the same time. ConcurrentMap doesn't support blocking operations like this directly.
If you need to minise locking to a specific key you can do the following.
ConcurrentMap<String, AtomicReference<Person>> map = new ConcurrentHashMap<String, AtomicReference<Person>>();
String name = "John";
AtomicReference<Person> personRef = map.get(name);
if (personRef == null)
map.putIfAbsent(name, new AtomicReference<Person>());
personRef = map.get(name);
if (personRef.get() == null)
synchronized (personRef) {
if (personRef.get() == null)
// can take a long time without blocking use of other keys.
personRef.set(new Person(name));
}
Person person = personRef.get();