AtomicReference
Complete guide to understanding and effectively working with AtomicReference class. Learn the differences among atomic assignment of references in Java, volatile variables and the AtomicReference class.
We'll cover the following
If you are interviewing, consider buying our number#1 course for Java Multithreading Interviews.
Overview
A reference type is a data type that represents an instance of a Java class, i.e. it is a type other than one of the Java’s primitive types. For instance:
Long myLong = new Long(5);
In the above snippet the variable myLong
represents a reference type. When we create the above object myLong
, Java allocates appropriate memory for the object to be stored. The variable myLong
points to this address in the memory, which stores the object. The address of the object in the memory is called the reference. Consider the below snippet:
Long myLong = new Long(5);
Long otherLong = myLong;
The two variables myLong
and otherLong
both point to the same memory location where the Long
object is stored or we can say both variables are assigned the same reference (address) to the object but not the object itself.
Given the above context, it becomes easier to understand the AtomicReference
class. According to the official documentation, AtomicReference
allows us to atomically update the reference to an object. Before we further delve into the topic, we’ll first clarify the distinction between AtomicReference
and assignment of references atomically in Java.
Atomic assignment of references in Java
Consider the following snippet:
class SomethingUseful {
Thread thread;
int myInt;
HashMap<String, String> map;
double myDouble;
long myLong;
void usefulFunction(int passedInt, long passedLong, double passedDouble, HashMap<String, String> passedMap) {
thread = Thread.currentThread();
myInt = passedInt;
map = passedMap;
}
}
If the method usefulFunciton()
is invoked by several threads, can we see garbage values for the variables of an instance of class SomethingUseful
? In the method usefulFunciton()
there are two primitive type assignments and two reference assignments. The following holds true for assignments in Java as per the language specification:
-
All reference assignments are atomic. This doesn’t mean our method
usefulFunction()
is thread-safe. It just means that when a thread assigns the variablesmap
or thethread
, the assignment is atomic, i.e. it can’t happen that the variablethread
ormap
hold some bytes from the assignment operation of one thread and other bytes from the assignment operation of an another thread. Whatever reference the two variables hold will reflect an assignment from one of the threads. Reference reads and writes are always atomic whether the reference itself consists of 32 or 64 bits. -
Assignments and reads for primitive data types except for
double
andlong
are always atomic. If two threads are invoking the methodusefulFunction()
and passing in 5 and 7 for the integer variable then the variablemyInt
will hold either 5 or 7 and not any other value. -
The reads and writes to
double
andlong
primitive types aren’t atomic. The JVM specification allows implementations to break-up the write of the 64 bitdouble
orlong
primitive type into two writes, one for each 32 bit half. This can result in a situation where two threads are simultaneously writing adouble
orlong
value and a third thread observes the first 32 bits from the write by the first thread and the next 32 bits from the write by the second thread. As a result the third thread reads a value that has neither been written by either of the two threads or is a garbage value. In order to make reads and writes todouble
orlong
primitive types atomic, we must mark them asvolatile
. The specification guarantees writes and reads to volatiledouble
andlong
primitive types as atomic. Note that some JVM implementations may make the writes and reads todouble
andlong
types as atomic but this isn’t universally guaranteed across all the JVM implementations.
So far we understood what a reference in Java means and that except for two primitive types all assignments are atomic in Java. This may confuse some readers as to the purpose and intent of the class AtomicReference
when assignments in Java are mostly atomic and the ones to double
and long
can be made atomic using volatile. We’ll address this question next.
AtomicReference
class
The class AtomicReference
promises assignment of a reference atomically, however, it offers far more in functionality than just an atomic assignment. Consider the snippet below:
// Regular variables
Thread thread;
int myInt;
// Assignments are indeed atomic, however, the method isn't thread-safe and the threads
// can potentially cache a stale value of the two variables in their cache.
void myFunction(int passedInt) {
myInt = passedInt;
thread = Thread.currentThread();
}
The variables thread
and myInt
can both be cached and a thread may not observe the latest value for them.
Next, we can mark the variable thread
as volatile.
// Marking Thread reference as volatile
volatile Thread thread;
// No change here.
int myInt;
// Assignments are atomic, however, the method isn't thread-safe. The memory
// visibility guarantees are now different. If a thread executes the function and then
// another thread reads the Thread reference, the reading thread will also see the latest
// value for myInt even though it is not marked as volatile.
void myFunction(int passedInt) {
myInt = passedInt;
thread = Thread.currentThread();
}
In the above snippet, the threads reading the thread
variable also see the latest value for the variable myInt
because the variable myInt
is assigned to before the thread
variable. Since thread
is marked volatile it establishes the happens-before relations for the variables that are assigned earlier than it.
We can rewrite the above snippet using the AtomicReference
class as follows:
// Using an atomic reference
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
// No change here.
int myInt;
// Assignments are atomic, however, the method isn't thread-safe. The memory
// visibility guarantees are similar to the snippet with the volatile thread
// variable.
void myFunction(int passedInt) {
myInt = passedInt;
threadAtomicReference.set(Thread.currentThread());
}
The above snippet is equivalent of the snippet with thread
marked as volatile.
Using AtomicReference
we establish the happen-before relationship similar to when a volatile
variable is written or read. Reading or writing to a volatile variable guarantees that the variable value is written to and read from the main memory and not the cache. Therefore all threads observing a volatile variable read the most up to date value for that variable and avoid using a stale value for the variable from their cache. The get()
and set()
operations on the AtomicReference
variable promise similar guarantees as the read and write of a volatile variable respectively.
However, also note that the assignment or initialization of an AtomicReference
doesn’t guarantee a happens-before relationship. For instance,
class SomeClass {
AtomicReference<Object> atomicReference;
public void init(Object obj) {
atomicReference = new AtomicReference<Object>(obj);
}
// ... Rest of class definition
}
In the above snippet, the atomicReference
is being initialized or assigned to and unless the atomicReference
variable is marked volatile, a happens-before relationship isn’t established.
check-then-act
The astute reader would question the usability of the AtomicReference
class if we can use volatile,
as we have done in our previous snippets, to achieve the same functionality. The crucial functionality AtomicReference
offers is to execute algorithms or actions that check the value of a variable and then act upon it based on the observed value, also known as check-then-act, atomically. We slightly modify our previous snippet to create a check-then-act scenario below:
// Marking Thread reference as volatile
volatile Thread thread;
int myInt;
// Assignments are atomic, however, the method isn't thread-safe. Now more
// than two threads can enter the if-clause even though the thread variable
// is marked volatile. Remember volatile doesn't mean thread-safety!
void myFunction(int passedInt) {
myInt = passedInt;
if (thread == null) {
thread = Thread.currentThread();
}
}
In the above scenario, we intend for a single thread to find the thread
variable null and initialize it. We want the two steps - check if thread
is null and act assignthread
- to be performed atomically, which volatile
can’t help us with. In fact, volatile
is only limited to offering stronger memory visibility guarantees and that’s about it. This is where AtomicReference
comes in and allows us to execute the two steps atomically. The same snippet re-written with AtomicReference
appears below:
// Using an atomic reference
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
int myInt;
// Assignments are atomic, however, the method isn't thread-safe. Only a
// single thread ever executes the if-clause.
void myFunction(int passedInt) {
myInt = passedInt;
if (threadAtomicReference.compareAndSet(null, Thread.currentThread())) {
thread = Thread.currentThread();
}
}
Take a moment to ponder on the above snippet and observe the following facts:
-
The method as a whole is still not thread-safe. The value of
myInt
may be from a thread that didn’t initialize thethreadAtomicReference
variable. -
If the variable
myInt
is moved after theif-clause
the happens-before isn’t established between variablesthreadAtomicReference
andmyInt
and the value ofmyInt
may only be updated in the cache and not the main memory. A thread that subsequently reads the variablethreadAtomicReference
will fetch all the variables visible to it from the main memory as per the volatile variable visibility guarantee but sincemyInt
was never updated in the main memory, the reader thread may observe a stale value for themyInt
variable. -
The initialization is guaranteed to be atomic and only a single thread ever enters the
if-clause
.
Example
In our code widgets below, we’ll construct three versions of the same example. We have several threads all attempting to simultaneously initialize a variable firstThread
to the current thread reference using Thread.currentThread()
. If the threads find the firstThread
variable null
it simply initializes the variable to itself. Here’s the first version using volatile variable:
Create a free account to view this lesson.
Continue your learning journey with a 14-day free trial.
By signing up, you agree to Educative's Terms of Service and Privacy Policy