AtomicMarkableReference
Understand the role of AtomicMarkableReference in Java concurrency as it helps maintain the state of nodes in lock-free linked lists. Explore how it uses a boolean mark alongside references to track logical deletions, preventing race conditions and inconsistent data during concurrent updates.
If you are interviewing, consider buying our number#1 course for Java Multithreading Interviews.
Overview
The AtomicMarkedReference class is similar to the AtomicStampedReference class in that it holds a reference to an object but instead of an integer stamp it takes in a boolean value, called the mark. Both these fields can be updated atomically either together or individually. One could argue that AtomicStampedReference class would behave similarly to AtomicMarkableReference class if it accepted only two possible values for the integer stamp argument.
The code widget below demonstrates some of the basic operations when working with the AtomicMarkableReference class.
Example
AtomicMarkableReference can be put to good use when designing lock-free lists, such as a set representation backed by a linked-list. In this lesson, we’ll not go through the implementation of a lock-free list but discuss parts of the implementation where the class AtomicMarkableReference is applicable.
Consider the SimpleNode class which represents a node in the linked list. The class uses an AtomicReference for the next node in the list, so that it can be updated atomically. The goal is to keep the list lock-free and update the next of the node correctly when a node is deleted. We’ll create a set-up that creates a race condition and causes the list to reach an inconsistent state.
Say we have three nodes linked as follows:
NodeA → NodeB → NodeC → null
Say we have two threads thread1 and thread2, each one trying to delete NodeA and NodeB respectively. The following sequence of events can cause the NodeC to not be deleted:
-
thread1stores thenextof NodeA, which points to NodeB and thenextof NodeB which points to NodeC in the variablesexpectedandnewNextrespectively. These variables serve as the expected and the new value for thecompareAndSet()method of theAtomicReference. -
At this point
thread1is suspended. We simulate that by sleeping the thread. -
thread2starts and attempts to delete NodeC.thread1computes the expected value for thenextof NodeB, which is NodeC itself and the new next value which should be null, since NodeC is the last node of the list. -
thread2continues and successfully deletes NodeC. -
thread1wakes up and proceeds to execute thecompareAndSet()call, which should succeed because NodeA still points to NodeB (the expected value) but the newnextpoints to NodeC which has been deleted. -
The program ends in a state where NodeC is not deleted and remains part of the list.
The above scenario is depicted in the code widget below:
The reason we end up in an inconsistent state is because we have no way of tracking the state of a given node if it has already been deleted or not. In our example, NodeC was deleted but thread1 didn’t realize that NodeC was not part of the list anymore. One possible solution is to store a boolean alongside each node that indicates the deletion status of the node. If set to true, the node is considered deleted but may not have been removed from the list yet.
This functionality is provided by the AtomicMarkableReference class. We can use AtomicMarkableReference to store the next for each node. Whenever, updating the next field of a node using the compareAndSet() method, we’ll pass in two expected values instead of one. The first expected value will be the reference of the object and the second a boolean value. When we update the next field of a node, we provide the new reference and false for the boolean value, implying that we only update the reference, if the node itself hasn’t been marked for deletion. In case, the delete operation finds that the predecessor of the node being deleted is already marked for deletion then the current delete operation fails physically, in the sense that the reference in the next field of the predecessor isn’t updated but the node being deleted has its mark set to true to indicate a logical delete.
If we re-run the same example with our modified algorithm, thread1 will succeed to update the reference after resumption from sleep, however, thread2 will not be able to delete nodeC. The list would still comprise of NodeA and NodeC but not be in an inconsistent state because the mark for NodeC will be true, indicating that the node has been logically deleted. The operation to delete NodeC can be either re-tried immediately or lazily deleted at a later time when another operation traverses the list and removes all the nodes marked true.
This example captures the gist of the utility of the AtomicMarkableReference class and demonstrates how additional state about a data structure can be maintained.