You are right that your code is not thread safe. This results in the following problems, with the nasty downside that most of the time it will work fine:
- Updates from one thread might never be shown to other threads.
- Two threads might check for a first name, find nothing and both add the lastname (before the change is shown to eachother).
- Theads might see 'incomplete' objects.
Basic Fix using synchronized
A very basic fix is to allow only one thread at the same time to access the concurrent part of your function, using the synchronized keyword (this can be added to the function definition, but you should use a private object to synchronize on).
public class Sample {
Map<String, String> firstNameToLastName = new HashMap<>();
private final Object nameMapLock = new Object();
public String testMethod(String firstName) {
synchronized(nameMapLock){
String lastName = firstNameToLastName.get(firstName);
if (lastName!= null)
return lastName;
String generateLastName = SomeAPI.generateLastName(firstName);
firstNameToLastName.put(firstName, generateLastName);
return generateLastName;
}
}
}
If multiple threads try to access the data at the same time, they have to wait until a another thread is done. You also have to make sure you don't introduce deadlocks in the locking.
Synchronizing on a private Object
In reply to the comments, I'll add a little explanation of why synchronization is done on a private object instead of on the full method (by adding synchronized
to the method definition) or on the map.
The reason for using a private object, is that you can be 100% sure that no other class is also using your object (read lock) to synchronize on.
When you use the synchronized
keyword on the method, you are in reality synchronizing on this
(the current object), meaning anyone using your class could also do that. When you synchronize on the map, the map itself might synchronize on that object as well, or other classes that you pass the map to.
Note that in some very rare cases you do want others to be able to use the same lock, but that means you have a lot of extra documenting to do, and run the risk of others misusing your locking.
The way I showed in the above example, is how most people do this. However there are plenty of other ways that would do the same.
Fix using ConcurrentHashMap
Using a ConcurrentHashMap
will solve problem 1 and 3 (as numbered above). But you still have to take special measure for point two. Since Java 8 you can do this quiet elegantly using ConcurrentHashMap.computeIfAbsent()
. This would work as follows:
public class Sample {
ConcurrentHashMap<String, String> firstNameToLastName = new ConcurrentHashMap<>();
public String testMethod(String firstName) {
return firstNameToLastName.computeIfAbsent(firstName,
name -> SomeAPI.generateLastName(name));
}
}
}
As you see this can make the implementation very elegant. However if you have more (and more complex) operations on the map, you might run into trouble.
synchronized
keyword. You have only one operation onfirstNameToLastName
but synchronized testMethod would block whole method not just one critical operation. – Alex Baranowski