The Answer by Solomon Slow is informative, and excellent.
In addition, I'd like to add that in modern Java we rarely need to address the Thread
class directly. The Executors framework was added in Java 5 to vastly simplify such code as yours.
The key concept is to separate the task(s) from the threads. Focus on the work to be by defining a task as a Runnable
(or Callable
if returning a result).
In your example, you seem to have two tasks that each result in incrementing a number, and you want to run each task twice. So let's define two classes that implement Runnable
. Both increment a counter, but only after pretending to do some amount of work. We simulate that work by sleeping some number of seconds. One sleeps a few seconds, the other sleeps longer, just to imagine two different workloads.
Both classes carry a private member field of an AtomicInteger
. That class provides thread-safe ways to increment a number. We need thread-safety protection because we are accessing the same number across threads.
We mark the AtomicInteger
member field as final
to prevent us from inadvertently re-assigning another object, as we might do during future edits to this code.
public class FastCalc implements Runnable
{
private final AtomicInteger counter = new AtomicInteger();
@Override
public void run ( )
{
System.out.println( "INFO - starting `run` on `FastCalc` at " + Instant.now() + " on thread ID " + Thread.currentThread().getId() ); // Beware: Output does *not* necessarily appear on console in chronological order.
try { Thread.sleep( ThreadLocalRandom.current().nextInt( 2_000 , 4_000 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
int currentCount = this.counter.incrementAndGet();
System.out.println( "INFO - result of `run` on `FastCalc` at " + Instant.now() + " is: " + currentCount );
}
public int report ( )
{
return this.counter.get();
}
}
And the slower version.
public class SlowCalc implements Runnable
{
private final AtomicInteger counter = new AtomicInteger();
@Override
public void run ( )
{
System.out.println( "INFO - starting `run` on `SlowCalc` at " + Instant.now() + " on thread ID " + Thread.currentThread().getId() ); // Beware: Output does *not* necessarily appear on console in chronological order.
try { Thread.sleep( ThreadLocalRandom.current().nextInt( 8_000 , 12_000 ) ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
int currentCount = this.counter.incrementAndGet();
System.out.println( "INFO - result of `run` on `SlowCalc` at " + Instant.now() + " is: " + currentCount );
}
public int report ( )
{
return this.counter.get();
}
}
Instantiate each of those tasks.
FastCalc taskFast = new FastCalc(); // Implements `Runnable`.
SlowCalc taskSlow = new SlowCalc(); // Implements `Runnable`.
Instantiate an ExecutorService
to handle the threading on our behalf. Usually we get an executor service by way of Executors
utility class.
Here we use Executors.newCachedThreadPool()
for an executor service that creates any number of threads as needed. This is appropriate in situations where we know we will use a limited number of threads.
ExecutorService executorService = Executors.newCachedThreadPool();
Your example runs each task twice. So we submit each task twice to our executor service.
Remember that both of our classes, FastCalc
& SlowCalc
, implements Runnable
. So we are passing Runnable
objects to the submit
method here.
executorService.submit( taskFast ); // Passing a `Runnable` object.
executorService.submit( taskSlow );
executorService.submit( taskFast );
executorService.submit( taskSlow );
Then we wait for the tasks to complete. We do this by calling a method that we pulled as boilerplate from the Javadoc of ExecutorService
. We changed that code a bit to pass Duration
as the amount of time we should reasonably wait for tasks to complete.
this.shutdownAndAwaitTermination( executorService , Duration.ofMinutes( 1 ) );
Here is that boilerplate.
void shutdownAndAwaitTermination ( ExecutorService executorService , Duration duration )
{
executorService.shutdown(); // Disable new tasks from being submitted
try
{
// Wait a while for existing tasks to terminate
if ( ! executorService.awaitTermination( duration.toSeconds() , TimeUnit.SECONDS ) )
{
executorService.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if ( ! executorService.awaitTermination( duration.toSeconds() , TimeUnit.SECONDS ) )
{ System.err.println( "Pool did not terminate" ); }
}
}
catch ( InterruptedException ex )
{
// (Re-)Cancel if current thread also interrupted
executorService.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
Lastly, we want to report on the results of the run.
System.out.println("Report — taskFast counter: " + taskFast.report() );
System.out.println("Report — taskSlow counter: " + taskFast.report() );
Pulling that code together.
package work.basil.example.threading;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class App2
{
public static void main ( String[] args )
{
App2 app = new App2();
app.demo();
}
private void demo ( )
{
System.out.println( "INFO - Start running demo. " + Instant.now() );
FastCalc taskFast = new FastCalc(); // Implements `Runnable`.
SlowCalc taskSlow = new SlowCalc(); // Implements `Runnable`.
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit( taskFast ); // Passing a `Runnable` object.
executorService.submit( taskSlow );
executorService.submit( taskFast );
executorService.submit( taskSlow );
this.shutdownAndAwaitTermination( executorService , Duration.ofMinutes( 1 ) );
System.out.println( "Report — taskFast counter: " + taskFast.report() );
System.out.println( "Report — taskSlow counter: " + taskFast.report() );
System.out.println( "INFO - End running demo. " + Instant.now() );
}
// Boilerplate pulled from Javadoc of `ExecutorService`.
// https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/concurrent/ExecutorService.html
void shutdownAndAwaitTermination ( ExecutorService executorService , Duration duration )
{
executorService.shutdown(); // Disable new tasks from being submitted
try
{
// Wait a while for existing tasks to terminate
if ( ! executorService.awaitTermination( duration.toSeconds() , TimeUnit.SECONDS ) )
{
executorService.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if ( ! executorService.awaitTermination( duration.toSeconds() , TimeUnit.SECONDS ) )
{ System.err.println( "Pool did not terminate" ); }
}
}
catch ( InterruptedException ex )
{
// (Re-)Cancel if current thread also interrupted
executorService.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
}
When run.
INFO - Start running demo. 2022-05-11T20:50:36.796870Z
INFO - starting `run` on `FastCalc` at 2022-05-11T20:50:36.809083Z on thread ID 16
INFO - starting `run` on `SlowCalc` at 2022-05-11T20:50:36.809228Z on thread ID 17
INFO - starting `run` on `SlowCalc` at 2022-05-11T20:50:36.808793Z on thread ID 15
INFO - starting `run` on `FastCalc` at 2022-05-11T20:50:36.808714Z on thread ID 14
INFO - result of `run` on `FastCalc` at 2022-05-11T20:50:40.081938Z is: 1
INFO - result of `run` on `FastCalc` at 2022-05-11T20:50:40.385796Z is: 2
INFO - result of `run` on `SlowCalc` at 2022-05-11T20:50:47.620290Z is: 1
INFO - result of `run` on `SlowCalc` at 2022-05-11T20:50:47.699582Z is: 2
Report — taskFast counter: 2
Report — taskSlow counter: 2
INFO - End running demo. 2022-05-11T20:50:47.703597Z
Regarding your original interest in total number of threads, we can see here by the thread ID numbers that this code uses a total of 4 threads, one thread per task submission.