1. Introduction
MapMaker is a builder class in Guava that makes it easy to create thread-safe maps.
Java already supports WeakHashMap to use Weak References for the keys. But, there is no out-of-the-box solution to use the same for the values. Luckily, MapMaker provides simple builder methods to use WeakReference for both the keys and the values.
In this tutorial, let's see how MapMaker makes it easy to create multiple maps and to use weak references.
2. Maven Dependency
First of all, let's add the Google Guava dependency, which is available on Maven Central:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.2-jre</version> </dependency>
3. A Caching Example
Let's consider a simple scenario of a server maintaining a couple of caches for the users: a session cache and a profile cache.
The session cache is short-lived with its entries becoming invalid after the user is no longer active. So the cache can remove the entry for the user after the user object is garbage-collected.
The profile cache, however, can have a higher time-to-live (TTL). The entries in the profile cache become invalid only when the user updates his profile. In this case, the cache can remove the entry only when the profile object is garbage-collected.
3.1. Data Structures
Let's create classes to represent these entities. We'll start first with the user:
public class User { private long id; private String name; public User(long id, String name) { this.id = id; this.name = name; } public long getId() { return id; } public String getName() { return name; } }
Then the session:
public class Session { private long id; public Session(long id) { this.id = id; } public long getId() { return id; } }
And finally the profile:
public class Profile { private long id; private String type; public Profile(long id, String type) { this.id = id; this.type = type; } public long getId() { return id; } public String getName() { return type; } }
3.2. Creating the Caches
Let's create an instance of ConcurrentMap for the session cache using the makeMap method:
ConcurrentMap<User, Session> sessionCache = new MapMaker().makeMap();
The returned map does not allow null values for both the key and the value.
Now, let's create another instance of ConcurrentMap for the profile cache:
ConcurrentMap<User, Profile> profileCache = new MapMaker().makeMap();
Notice that we have not specified the initial capacity for the caches. So, MapMaker creates a map of capacity 16 by default. If we want to we can modify the capacity using the initialCapacity method:
ConcurrentMap<User, Profile> profileCache = new MapMaker().initialCapacity(100).makeMap();
3.3. Changing the Concurrency Level
MapMaker sets the default value for the concurrency level to 4. However, the sessionCache needs to support a higher number of concurrent updates without any thread contention. Here, the concurrencyLevel builder method comes to the rescue:
ConcurrentMap<User, Session> sessionCache = new MapMaker().concurrencyLevel(10).makeMap();
3.4. Using Weak References
The maps we created above use strong references for both the keys and values. So, the entries stay in the map even if the keys and the values are garbage-collected. We should use weak references instead.
A sessionCache entry is invalid after the key (the user object) is garbage-collected. So, let's use weak references for the keys:
ConcurrentMap<User, Session> sessionCache = new MapMaker().weakKeys().makeMap();
For the profileCache, we can use weak references for the values:
ConcurrentMap<User, Profile> profileCache = new MapMaker().weakValues().makeMap();
When these references are garbage-collected, Guava guarantees that these entries will not be included in any of the subsequent read or write operations on the map. However, the size() method might sometimes be inconsistent and can include these entries.
4. MapMaker Internals
MapMaker creates a ConcurrentHashMap by default if weak references are not enabled. The equality checks happen via the usual equals method.
If we enable weak references, then MapMaker creates a custom map represented by a set of hash tables internally. It also shares similar performance characteristics as a ConcurrentHashMap. However, an important difference with WeakHashMap is that the equality checks happen via the identity (== and identityHashCode) comparisons.
5. Conclusion
In this short article, we learned how to use the MapMaker class to create a thread-safe map. We also saw how to customize the map to use weak references.
As always, the full source code of the article is available over on GitHub.