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:
- fetch the value of the
counter
variable, which is a reference to an immutableInteger
object. - Get the
int
value of theInteger
object, - Compute a new
int
value, equal to 1 the previous value, - Construct a new
Integer
object with the new value, and finally - 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