Home > Software design >  How does incrementing a value across multiple threads in Java work?
How does incrementing a value across multiple threads in Java work?

Time:03-05

I have two pieces of multithreaded code. One passes an Integer to the thread and the thread increments it and the other passes an object that has an inner Integer which is incremented. When I run these the output is different and not what I expected. Can someone give an explanation of what's going and how I'd make the first behave in a similar manner to the second.

Implementation 1:

public class Main {
    public static void main(String[] args) {
        Integer counter = 0;
        for (int i = 0; i < 3; i  ) {
            Threaded t = new Threaded("Thread "   i, counter);
            Thread thread = new Thread(t);
            thread.start();
        }
    }
}

class Threaded implements Runnable {
    private final String name;
    private Integer counter;
    
    public Threaded(String name, Integer counter) {
        this.name = name;
        this.counter = counter;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i  ) {
            counter  ;
            System.out.println(name   ": "   counter);
        }
    }
}

Output 1:

Thread 1: 1
Thread 1: 2
Thread 0: 1
Thread 0: 2
Thread 0: 3
Thread 0: 4
Thread 0: 5
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 2: 5

Implementation 2:

public class Main {
    public static void main(String[] args) {
        MyObj obj = new MyObj();
        for (int i = 0; i < 3; i  ) {
            Threaded t = new Threaded("Thread "   i, obj);
            Thread thread = new Thread(t);
            thread.start();
        }
    }
}

class MyObj {
    private int count = 0;
    
    public void inc() { count  ; }
    
    public int getCount() { return count; }
}

class Threaded implements Runnable {
    
    private final String name;
    private final MyObj obj;
    
    public Threaded(String name, MyObj obj) {
        this.name = name;
        this.obj = obj;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i  ) {
            obj.inc();
            System.out.println(name   ": "   obj.getCount());
        }
    }
}

Output 2:

Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 1: 6
Thread 0: 1
Thread 0: 7
Thread 2: 8
Thread 0: 9
Thread 2: 10
Thread 2: 11
Thread 0: 12
Thread 2: 13
Thread 2: 15
Thread 0: 14

Is it possible to have multiple threads all increment an Integer directly but get an output similar to implementation 2?

CodePudding user response:

For the first example, each thread has its own counter and it is incremented separately. Integer is immutable, when it is incremented the result is a new Integer object. The threads get the same starting value but there is no shared state.

For the second example the threads share the same MyObject instance, there is nothing to stop them from overwriting each others' work. Use AtomicInteger in the second example and the threads won't interfere with each other.

class MyObj {
    private AtomicInteger counter = new AtomicInteger(0);

    public void inc() { 
        counter.incrementAndGet(); 
    }

    public int getCount() { 
        return counter.get();
    }
}

CodePudding user response:

In a nutshell: The difference between your two implementations is that your MyObj class is mutable, but Integer is immutable.

In particular, this statement does not do what you seem to think it does:

counter  ;

What it does is:

  1. fetch the value of the counter variable, which is a reference to an immutable Integer object.
  2. Get the int value of the Integer object,
  3. Compute a new int value, equal to 1 the previous value,
  4. Construct a new Integer object with the new value, and finally
  5. Store a reference to the new object into the counter variable.

Since each of your threads has its own counter, and each one constructs its own new sequence of Integer objects to assign to counter, the threads run entirely independent of each other.


In your second example, there is only ever one MyObj instance. Each thread has its own obj variable, but those are all initialized to point to the same instance, and the threads never assign their obj variables: The threads mutate the one shared instance instead.

CodePudding user response:

Never share unprotected resources across threads

The Answer by Nathan Hughes is correct.

Change this code:

new Threaded( "Thread "   i , obj )

… to this:

new Threaded( "Thread "   i , new MyObject( … ) )

… to get behavior similar to your first example. By passing each new Thread its own instance of MyObject, the various Thread objects will no longer be stepping in each other’s feet.

The lesson here is to never share resources across threads without adding protection. As Nathan Hughes suggested, replacing int on MyObject class with AtomicInteger is one way to add protection. Mark that AtomicInteger as final to ensure that its instance is never replaced.

Executor service

Another issue: In modern Java, we rarely address the Thread class directly. Instead, use the Executors framework added to Java 5.

Define your task as a Runnable or Callable. Pass an instance of that task to an executor service.

The lambda syntax in Java 8 is often quite convenient to use in defining your Runnable. The code in the lambda can refer to variables in its surrounding code.

This has been covered many times on Stack Overflow. Search to learn more.

Example code

Here is a revamped version of your code using an executor service.

Notice that inc method has been changed to return the newly incremented count rather than returning void. By the time another line of code called the MyObj#getCount, some other thread may have performed an additional increment, so result of getCount would not give your intended results.

package work.basil.threading;

import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class App2
{
    public static void main ( String[] args )
    {
        App2 app = new App2();
        app.demo();
    }

    private void demo ( )
    {
        final MyObj myObj = new MyObj();

        Runnable task = ( ) -> {
            for ( int i = 0 ; i < 5 ; i   )
            {
                int newValue = myObj.inc();
                // BEWARE: Multi-threaded calls to `System.out` may *not* appear chronologically. Add timestamp to get some idea of chronology.
                System.out.println( Thread.currentThread().getId()   " thread incremented to : "   newValue   " at "   Instant.now() );
            }
        };

        ExecutorService executorService = Executors.newCachedThreadPool();
        for ( int i = 0 ; i < 3 ; i   )
        {
            executorService.submit( task );
        }
        executorService.shutdown();
        try { executorService.awaitTermination( 1 , TimeUnit.MINUTES ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
    }
}

class MyObj
{
    private final AtomicInteger count = new AtomicInteger();

    public int inc ( ) { return this.count.incrementAndGet(); }

    public int getCount ( ) { return count.get(); }
}

Example run. Again, beware of the console printing lines out of chronological order.

15 thread incremented to : 1 at 2022-03-04T21:54:17.926156Z
17 thread incremented to : 3 at 2022-03-04T21:54:17.926207Z
16 thread incremented to : 2 at 2022-03-04T21:54:17.926219Z
17 thread incremented to : 5 at 2022-03-04T21:54:17.941980Z
15 thread incremented to : 4 at 2022-03-04T21:54:17.941961Z
16 thread incremented to : 6 at 2022-03-04T21:54:17.942027Z
15 thread incremented to : 8 at 2022-03-04T21:54:17.942054Z
17 thread incremented to : 7 at 2022-03-04T21:54:17.942041Z
16 thread incremented to : 9 at 2022-03-04T21:54:17.942067Z
15 thread incremented to : 10 at 2022-03-04T21:54:17.942083Z
17 thread incremented to : 11 at 2022-03-04T21:54:17.942126Z
16 thread incremented to : 12 at 2022-03-04T21:54:17.942164Z
15 thread incremented to : 13 at 2022-03-04T21:54:17.942176Z
16 thread incremented to : 15 at 2022-03-04T21:54:17.942254Z
17 thread incremented to : 14 at 2022-03-04T21:54:17.942219Z
  • Related