2

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

3
  • 7
    Of course 2 2 can happen as well: t1 incremented first without race (n = 1), then t2 incremented without race (n = 2) (or vice versa), and then t1/t2 read the current value (n = 2), and then printed it. Commented Oct 22, 2024 at 9:32
  • 3
    Hello pochopsp, in reality, no matter what the results may be, we are facing a wrong implementation, the result is unpredictable, being n static, the correct thing would be that the access to n is synchronized. Commented Oct 22, 2024 at 13:03
  • @MarcePuente thanks for your comment but I knew that this code is not synchronized and results is unpredictable, that's why I asked. I wanted to know all the possible outputs for this code, to understand what happens in absence of synchronization Commented Oct 23, 2024 at 8:29

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks for your detailed answer Basil. As I already pointed out, my question is "moot" on purpose: it's an old university exercise I've stumbled upon. Thanks for confirming my scenarios and for exploring further the 2 2 one. To make an experiment I've run 100k times the program on my machine and I've observed 50197 times 1 2, 49801 times 2 1 and just two times 1 1 with no observation of 2 2 (and that contributed to my doubts about it)
Also, given different jvm and CPU implementations, it's entirely possible one flavor of jvm or CPU combination will never give 2 and 2 for one reason or another, so brute force testing isn't really ever going to show all possible situations. What matters is that it's not threadsafe, and Basil has demonstrated how 2 and 2 might be possible given the implementation of the ++ operator.
@RyanLeach I agree, except for the last clause of your last sentence. The 2 & 2 result does not stem from the ++. That result comes from a pause after the ++ line is complete and before the read of n in the m = n line.
@pochopsp I am unaware of any specific reason why the 2 & 2 would not happen. But today’s compilers, JVM, OSes, and CPUs do some wild things. All you can count on is the Java specs. It is the duty of the JDK authors to ensure that the Java specs are respected without exception. Regarding brute-force testing of concurrency, that can be helpful but is not proof.
@BasilBourque got it. The point of the university exercise was just to reason about every possible scenario that can happen given the code (I think just to make students understand how interleaving between threads could go in any way and that volatile would then grant the visibility). It was great reading your answer and reasoning about it and (of course) also thinking about what would be a correct implementation if we wanted deterministic output.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.