Suppose we have a map that is shared between multiple threads. It represents a node in some hierarchical structure (say, a directory in a file system) that is stored on disk. Constructing a Value is expensive both in time and in memory. This is the classic 'initialize on demand' problem, with a twist: we're initializing values in a map when there's lookup request, but we don't want to lock the entire map while we're doing this in order to let other threads access already constructed values. In this application, lookups of existing Values will be much more common then non-existing ones.
Attempt 1: Grab write lock on map, check for presence of Key, return if present, otherwise, construct Value, put in map.
Evaluation: This prevents other threads from accessing the map while we're constructing the Value. Since reads are very common and very fast, this will manifest itself as an ugly jump in latency: not good.
Attempt 2: Grab read lock on map, check for presence of Key, return if present, otherwise release read lock, construct Value, grab write lock, check for presence, if not present, put in map, if present delete newly constructed Value.
Evaluation: Now we don't incur the jump in read latency, but we might end up needlessly constructing several in memory representations of the same Value (when multiple threads try to look up the same yet-unconstructed Value simultaneously). Problem is, we don't want this: these Values are really expensive to create. Besides, they might start firing events or doing I/O, which means that now we have to deal with a design that allows ephemeral yet heavyweight Value instances to exist: an additional headache better avoided.
Attempt 3: Use two-levels of locks. Grab read lock on map, lookup Key, return if present, otherwise, release read lock on map, grab write lock on map, check for placeholder, grab placeholder's read lock, if present, otherwise, create placeholder, grab its write lock, insert into map, release write lock on map, create Value, replace placeholder with Value, release placeholder's write lock.
Evaluation: Inserting a placeholder into the map while constructing the Value guarantees that only one thread will attempt it, thus addressing the problems of Attempt 2. However, this design leaves open the question: what form this placeholder should take? The answer is not trivial. First, if it contains a lock and threads wait on it, it becomes hard to delete it (How do you know nobody's holding the placeholder's lock? You can grab it, sure, but then you are holding it so, again, you cannot delete it). Second, there can be lookups of Keys for which no Value exists on disk. If we insert a placeholder for every lookup attempt (even for ones that will eventually fail) then who's going to clean these up? Finally, with two levels of locks, the code becomes quite ugly: we first grab read lock, check, then acquire write lock, re-check, and we need to do this both at the map level, and the individual placeholder level (easy to create deadlocks, I can attest to it).
This little baby keeps popping up and I can't seem to find an elegant solution. Attempt 3 is the closest I got to satisfying my performance conditions, but it's ugly and error prone once you get down to the dirty details. I would really appreciate any insights or suggestions.