Search code examples
javamultithreadingoutputvolatile

Java volatile and multithreading: what are the possible outputs for the program?


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:

  • t1 and t2 are executed completely (doesn't matter in which order) and not in parallel so we have 1 2
  • t1 and t2 are executed in parallel, in both threads n++ gets executed "correctly" but the scheduler executes first the print for the thread which has m=2 so we have 2 1
  • t1 and t2 are executed in parallel and both read 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) ?


Solution

  • ++ increment operator considered non-atomic

    Your 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++;:

    • Read the current value from n. (Making a copy of that value, essentially)
    • Add 1 to that copied-value.
    • Store the sum back into the variable.

    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.

    Scenarios

    As for your scenarios:

    • Yes, if one thread completes before the other starts, you will get 1 & 2 as output.
    • If the threads run simultaneously on separate cores, or if they run alternating on a single core, their operations may interleave.

    If we combine the facts discussed above:

    • Increment operator is non-atomic
    • The incrementing line and the m = n line are non-atomic

    … then we have sequence of four n-related steps that may be interleaved:

    1. Read value of n, making a copy.
    2. Increment the copied-value.
    3. Write the incremented-copied-value into the variable n.
    4. Read the value in variable 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
      Both threads read 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
      One thread reads 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
      One thread reads 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

    Moot

    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.)

    Thread-safe code

    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.