Real-life problem (Java Spring application)
We have a stateless application that integrates with a third party system. Our app identifies users by a JWT token. Whenever calls to the third party system are made from inside our app (as a result of user-made HTTP calls), it needs to create a session for the user towards the outside system. This session should be reused and there should be a one-to-one mapping between it and the user.
What happens when we have, for example, two parallel calls made by one user towards our application? There is a possibility for the aforementioned one-to-one relationship to be broken and multiple sessions to be created for that same user. We want the second thread/call to reuse the session created by the first. Basically, we want our “session creation/retrieval” block to be thread synchronized by the user ID (which we retrieve from the JWT token).
We know that Java’s synchronized
block locks by Object
reference. In our case, we want the value of the user ID to serve as a locking mutex. Of course, we cannot change the behavior of Java’s synchronized
block. Nor can we lock by a Session Object
, since our application is stateless and does not leverage the use of one.
Solution
So, what are we to do?
Let us keep our user IDs in a HashMap
. That way, we can retrieve the same Object
reference for a given user ID from the map and lock by it.
There are two impediments in this approach:
- We need the map to be able to handle concurrency/multiple threads.
- We need the map to dynamically remove stale entries (thus releasing memory) when they are not used for synchronization. This is very important for large applications with millions of users. We do not want to get memory overflows. Java’s weak references will come in handy here.
There is a ConcurrentHashMap
in Java, as well as a WeakHashMap
(which leverages the native functionality of Java’s WeakReference
type). In our situation – we want both. We can make use of Collections.synchronizedMap(new WeakHashMap<>())
, but its performance is not as good as one would think.
Luckily, there exists a ConcurrentReferenceHashMap
class inside the maven package org.hibernate:hibernate-validator
.
Note: This map is not to be confused with the one coming from Spring with the same name. That one does not suit our case scenario and is mainly intended for internal Spring-related functionalities.
Here is the maven dependency:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
If you do not want to add the whole dependency to your code you can just copy and paste the map class directly. It contains no additional external dependencies towards outside classes.
Note: If you are using org.springframework.boot:spring-boot-starter-validation
, you already have the hibernate-validator
as a transitive dependency in your classpath
.
Next, create XMutexFactory
class :
import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap;
import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap.ReferenceType;
public class XMutexFactory<T> {
private static final int INITIAL_CAPACITY = 16;
private static final float LOAD_FACTOR = 0.75f;
private static final int CONCURRENCY_LEVEL = 16;
private static final ReferenceType REFERENCE_TYPE = ReferenceType.WEAK;
private final ConcurrentReferenceHashMap<T, XMutex<T>> map;
public XMutexFactory() {
map = new ConcurrentReferenceHashMap<>(
INITIAL_CAPACITY,
LOAD_FACTOR,
CONCURRENCY_LEVEL,
REFERENCE_TYPE,
REFERENCE_TYPE,
null
);
}
public XMutex<T> getMutex(T key) {
return map.computeIfAbsent(key, XMutex::new);
}
}
There is only one method here (getMutex
), which will be used for retrieving the locking reference for the synchronized
block.
The important part to note here is the REFERENCE_TYPE
we send to the map’s constructor. This should be set to WEAK
for both keys
and values
(hence the 18th and 19th lines of code from the snippet above).
The XMutex
class will serve as the value
and as a wrapper around the key
(read on until the end to find out why this is needed).
public class XMutex<T> {
private final T key;
public XMutex(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
The only thing left to do now is to use this factory when coding our synchronized
blocks.
You can create a bean
(if you are using Spring) and inject it where necessary, or you can just create your own specific singleton of this factory.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class XMutexFactoryConfiguration {
@Bean
public XMutexFactory<String> stringMutexFactory() {
return new XMutexFactory<>();
}
}
Example usage:
public Session getSession() {
String userId = userService.getLoggedInUserID();
synchronized (stringMutexFactory.getMutex(userId)) {
// Create session. If one is already present, return that one.
// ...
}
}
That is it! So, you basically just call stringMutexFactory.getMutex("whatever value you want" /* in our case it is the user ID */)
which will serve as the locking reference. This method will return us a mutex
for the current user.
You might be asking: how are we certain that the garbage collector (GC) will not delete the user’s map entry while one of the threads still executes the synchronized
block? We are using weak references for our keys
and values
, after all. That means that whenever we lack strong references to a given object, that object is liable for removal. If the GC deletes our key
or value
during the execution of a particular thread, the thread coming in second will get a new freshly created mutex
and enter the synchronized
block in parallel with the first. If that were bound to happen, then we have achieved nothing. Luckily, our design of the <T, XMutex<T>>
pair will not allow it.
Take a look at the following image (pink lines are weak references; black lines are regular strong references):
As you can see, the synchronized
block has a strong reference towards XMutex(key)
. XMutex(key)
in turn has a strong reference to the actual key
. So, while the synchronized
block “lives”, there will be two strong references in existence – one towards the value
and one towards the key
. After the synchronized
block ends, the mutex
can be removed from memory by the GC. This means that the key: "abc"
can also be removed afterwards, since there would be no more strong references to it (that is, if the application does not reference that same key somewhere else in the code). Only the couple of weak references coming from the internal structure of the map would remain, hence both XMutex(key)
and key: "abc"
will be liable for deletion.
The map will eventually – in the background – also clear/release (for garbage collecting) the “empty” WeakReference
objects (by “empty” we mean WeakReference
s that have null
as a referent
). This will also clear up some additional memory.
Note: Whenever the map happens to contain a mapping for a given key
to a null
value
(in our case this can happen after XMutex(key)
is deleted from memory and the WeakReference(value)
in the map is left with null
as a referent
), it is bound to release (for garbage collecting) both of the WeakReference
objects. It does not matter if the key: "abc"
is still in memory and if it might still have strong references to it from the outside. Mapping a key
to null
brings no worth and so the map will cut both of the weak references, thus releasing additional memory.
We might even do something like this (instead of creating a wrapper XMutex(key)
, we will use the same key
as value
):
The problem with this is that whenever there is a strong reference (for whatever reason) to the key
(in our case the user ID), the WeakReference(value)
inside the map will not be predisposed for removal, hence the whole entry will keep its memory in the map. We do not have that weakness in the first example. We want to use the value
for synchronization purposes only, so there is no sense for it being the same reference as the key
.
Summary
To wrap up, you only need to do a small number of things to leverage the power of “value” synchronization (mind the quotes; we are only making it look as if we are using values instead of references as mutexes):
- Add the dependency for the map.
- Copy and paste the XMutex class.
- Copy and paste the XMutexFactory class.
- Inject the factory where you need it and use it for mutex retrieval and synchronization.
Keep in mind that this value can be whatever Object
, not just a user ID String
as per our example. The important thing is for the “value”-class to implement hashCode
and equals
, so that it can be used as a key
inside the ConcurrentReferenceHashMap
.
A big pro to this solution is that it is not Spring framework specific. One can use this same architecture down to the most basic console Java applications.
Additional resources
For background knowledge, it would be good for one to read about Strong
, Soft
and Weak
references in Java. General knowledge about thread synchronization is also a must.