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:

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

    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:

    If we combine the facts discussed above:

    … 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:

    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.