I've stumbled upon an old university exercise in which the aim was to reason about what can happen in absence of synchronization.
Consider the following code:
public class Main {
public static void main(String args[]){
Thread t1 = new SimpleThread();
Thread t2 = new SimpleThread();
t1.start();
t2.start();
}
static class SimpleThread extends Thread {
private static volatile int n = 0;
public void run() {
n++;
int m = n;
System.out.println(m);
}
}
}
What are all the possible outputs for this program and why?
I've thought of the following scenarios:
1 2
n++
gets executed "correctly" but the scheduler executes first the print for the thread which has m=2
so we have 2 1
n=0
in the instruction n++
so we have 1 1
Did I get these scenarios right? Is there some scenario I am missing out (e.g. can 2 2
happen and why)
?
++
increment operator considered non-atomicYour code n++
is not atomic. Or so we must assume given the definition of the Postfix Increment Operator ++
in Java. See the Java Language Specification where no promise of thread-safety or atomicity is made. See also Why is i++ not atomic?.
So there are three moments in time there in your short line n++;
:
n
. (Making a copy of that value, essentially)1
to that copied-value.Any amount of time may elapse between those moments. During that time, another thread may access and/or alter the value within the variable. So the first thread could stomp on the value set by the second thread.
volatile
guarantees visibility, but not thread-safety
private static volatile int n = 0;
The static
there means that only one variable named n
exists within the app.
n++;
&int m = n;
Any amount of time may elapse between those two lines. During that time, another thread may change the value of our one-and-only n
variable.
The volatile
on the line at top means the current thread will see any changes, guaranteed.
So the value set in the first of this pair of lines may well differ from the value accessed in the second line.
As for your scenarios:
1
& 2
as output.If we combine the facts discussed above:
m = n
line are non-atomic… then we have sequence of four n
-related steps that may be interleaved:
n
, making a copy.n
.n
(copying, to be stored in m
).Let’s show that as comments in the code:
// We have a Read, Increment, Write, and Read.
n++; // Read, making a copy. Increment copied-value. Write sum back into variable.
int m = n; // Read, making a copy. This is a fresh read. Value found here is NOT necessarily the same value as result of line above, if other threads alter the contents of the variable.
Interleaving scenarios include:
1
& 1
0
from variable, and write 1
into variable. The second writer stomps on first writer. Console output is 1
& 1
.1
& 2
or 2
& 1
0
, and writes 1
, and reads 1
. The other thread reads 1
and writes 2
, and reads 2
. No stomping as the reading-writing is sequential. But be aware that in this case the successive println
calls may not be sequential. The println
calls may interleave. So the console output may be in either order: 1
& 2
or 2
& 1
.2
& 2
0
, and writes 1
… but then other thread reads 1
and writes 2
… and first thread reads 2
. That first thread wrote 1
but before retrieving the current value of n
for the m = n
assignment, the value in n
had been changed by another thread.This last case of 2
& 2
was not covered in your Question. Thanks to Mark Rotteveel for pointing out this case in a Comment. Took me a while to see it!
You can force the case of 2
& 2
by adding a Thread.sleep
between the increment line and the copy-assignment line.
n++;
try { Thread.sleep ( Duration.ofMillis ( 7 ) ); } catch ( InterruptedException e ) { throw new RuntimeException ( e ); } // ⬅️ Force the "2 & 2" case by inserting a `Thread.sleep`.
int m = n;
System.out.println ( m );
Where run:
2
2
As Marce Puente commented, your question is moot.
Attempting to write this Answer was a fun challenge, but ultimately pointless. As soon as we spot the unprotected variable n
being accessed and altered across threads, we know the code is not thread-safe, and the result unpredictable. So in real work we would focus on making that code thread-safe, not delineating all possible forms of wonky results.
When pasting your code into IntelliJ, I immediately get a warning on the n++
saying:
Non-atomic operation on volatile field 'n'
(By the way, I also got a flag about your C-style array declaration, String args[]
. In Java, String[] args
seems more common, by convention, not a technicality.)
To make a thread-safe version of your code, we need to fix that non-atomic operation on an unprotected resource, our int n
variable.
One way to protect is to use synchronized
or a lock to guard access to your n
variable. This is a legitimate approach, but not my preference.
I generally prefer to use the Atomic…
classes. The presence of the class name shouts to the reader that we have a thread-safety issue to address here. Then, having to call the Atomic…
methods to do any kind of work is a constant reminder to the programmer that the resource being accessed/altered is protected for thread-safety.
So I would replace your int n
with AtomicInteger n
.
We do not need volatile
on that variable declaration if we populate it on the declaration line. The Java Language spec guarantees that no access to n
is possible until the declaration line completes execution.
Since n
is static
, we only a single instance at a time. But we actually want a Singleton, as we never want that instance replaced. We want our app to only ever have one, and only one, instance of AtomicInteger
in the variable n
. So let’s ensure that, and document that, by marking the declaration as final
.
So, this:
private static volatile int n = 0;
… becomes:
private static final AtomicInteger n = new AtomicInteger ( 0 );
Then we change these 3 lines:
n++;
int m = n;
System.out.println ( m );
… to these 2 lines:
int m = n.incrementAndGet ( );
System.out.println ( n.incrementAndGet ( ) );
Of course we could collapse that to a single line System.out.println ( n.incrementAndGet ( ) );
but that is besides the point of your Question.
Full example code:
package work.basil.example.threading;
import java.util.concurrent.atomic.AtomicInteger;
public class Scenarios
{
public static void main ( String[] args )
{
Thread t1 = new SimpleThread ( );
Thread t2 = new SimpleThread ( );
t1.start ( );
t2.start ( );
}
static class SimpleThread extends Thread
{
private static final AtomicInteger n = new AtomicInteger ( 0 );
public void run ( )
{
int m = n.incrementAndGet ( );
System.out.println ( m );
}
}
}
Caveat: I am not an expert on this. For expertise, see the book Java Concurrency in Practice.