Learn MultiThreading in Java/Spring Boot

This tutorial will cover: Feel free to skip the background sections on multithreading outside of SpringBoot. If you have the time, the background can help build your foundation.

Prerequisites



Intro to MultiThreading and Thread Pools

A thread is a sequence of programmed instructions that can be managed by the Operating System's scheduler. A program can spawn multiple threads, which are then managed by the operating system. At any given time, you can have as many threads running as you have cores in your CPU(s). If we write code that uses more threads than we have cores, the OS will schedule these threads accordingly.

How to Choose the Right Number of Threads

CPU intensive tasks - Since we can only have as many threads actually running as we have cores, if our task is CPU intensive, we should limit the number of threads to at most the number of cores on our system. If other processes need threads too, we may need to reduce number of threads to less than the number of cores.
Network or I/O intensive tasks - If our threads are making HTTP calls and waiting on a response, or waiting for slow reads or writes to a disk, we may opt to have more threads than number of cores because cores are not the limiting speed factor.

Vanilla Java: Using Executors to Create Single Threads

As explained in my previous tutorial, let's create a SpringBoot application named MultiThreadedApplication.
We can generate a single thread by creating a Single Thread Executor. Then, as shown below, we call the execute method which takes as input any class that implements the runnable interface, or a lambda expression, which is what we use here. If you place the code as shown below in the main method of your Spring Boot application, prior to SpringApplication.run, you can run some code in a separate thread while Spring Boot starts up!
Go ahead and do this and start up your application.

@SpringBootApplication
public class MultiThreadedApplication {
	public static void main(String[] args) {
		Executor executor = Executors.newSingleThreadExecutor();
		executor.execute(() -> {try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} System.out.println("Hello World");});
		SpringApplication.run(MultiThreadedApplication.class, args);
	}
}
				
You should see "Hello World" printed after to Spring Boot's ASCII art.
If we want to add alot of logic to be executed by this thread, call an API, or access a database, it might be better to have this all defined in a separate class. So let's see how to do that. Create a class such as this:

public class RunnableObject implements Runnable {
	
	String name;
	
	public RunnableObject(String name) {
		this.name = name;
	}

	@Override
	public void run() {
		System.out.println("Started thread " + name);
		try {
			Thread.sleep(2000); //wait 2 seconds
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		System.out.println("Ended thread " + name);
	}
}
Now rewrite your main method to look like this:

@SpringBootApplication
public class MultiThreadedApplication {

	public static void main(String[] args) {
		Executor executor = Executors.newSingleThreadExecutor();
		RunnableObject runnable = new RunnableObject("mythread");
		executor.execute(runnable);
		SpringApplication.run(MultiThreadedApplication.class, args);
	}

}
				
Start up your application again. You should see Started thread mythread and then during or after SpringBoot finishes booting up, you should see Ended thread mythread.

Vanilla Java: Fixed Thread Pool

A bi-threaded app using a single thread executor might be useful when we need to perform a series of steps that must be done sequentially, while also doing other tasks in another thread. What if we want to use more than two threads at a time? This is where Thread Pools come into play. A thread pool consists of 1 or more threads and queue. The queue holds tasks until threads are ready. Thread Pools have some key properties:
  • Core Pool Size - the number of threads that are always in existence
  • Max Pool Size - the max number of threads that can be generated
  • Keep Alive Time - how long idle threads are allowed to exist
  • Allow Core Thread Timeout - Usually false. If set to true, core threads can be killed after keep alive time passes
First let's look at the fixed thread pool. In the fixed thread pool, the core pool size is equal to the max pool size. The pool maintains the same amount of threads at all times. In this thread pool, the core pool size is equal to the max pool size. If one fails due to an exception, a new one is started. The threads in the pool will exist until .shutdown() is called. Try replacing the code in your app class with this and running your app.

@SpringBootApplication
public class MultiThreadedApplication {

	public static void main(String[] args) {
		
		ExecutorService executorService = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 10; i++) {
			Runnable runnable = new RunnableObject("myThread_" + i);
			executorService.execute(runnable);
		}	
		executorService.shutdown();
		
		SpringApplication.run(MultiThreadedApplication.class, args);
		
		while (!executorService.isTerminated())	{
			// wait
		}
		System.out.println("All threads completed successfully");
	}
}
					
You should see something like:
2020-09-12 13:35:33.151  INFO 17976 --- [           main] c.e.demo.CompletableFuture2Application   : Started CompletableFuture2Application in 1.172 seconds (JVM running for 1.803)
Ended thread myThread_1
Ended thread myThread_2
Ended thread myThread_0
Ended thread myThread_3
Started thread myThread_8
Ended thread myThread_4
Started thread myThread_7
Started thread myThread_6
Started thread myThread_5
Started thread myThread_9
Ended thread myThread_9
Ended thread myThread_8
Ended thread myThread_7
Ended thread myThread_6
Ended thread myThread_5
All threads completed successfully
You can see once the first five threads are created (prior to SpringBoot launching), at least one thread must end before another one can start.

Cached Thread Pool

There is another type of thread pool in Java called cachedThreadPool which will generate an unlimited amount of threads. The core pool size of cached thread pool 0 and the max pool size is Integer.MAX_VALUE. As explained in the oracle docs, "Calls to execute will reuse previously constructed threads if available. If no existing thread is available, a new thread will be created and added to the pool. Threads that have not been used for sixty seconds are terminated and removed from the cache." Try putting rewriting your application class with this code:

@SpringBootApplication
public class MultiThreadedApplication {

	public static void main(String[] args) {
		ExecutorService service = Executors.newCachedThreadPool();
		for (int i = 0; i < 30; i++) {
			service.execute(new RunnableObject("My Thread Number " + i));
		}
		SpringApplication.run(MultiThreadedApplication.class, args);
	}

}
					
You should see 30 threads started, then after Spring Boot starts up, the 30 threads finish.

Scheduled Thread Pool

A scheduled thread pool contains a fixed number of threads, which can run tasks at specified regular time intervals. The core pool size of this thread pool is defined in the constructor, and the max pool size is Integer.MAX_VALUE. Let's rewrite our code again to use a scheduled thread pool.
First let's modify our runnable object a bit to log system time as shown.

package com.example.demo;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class RunnableObject implements Runnable {
	
	String name;
	
	public RunnableObject(String name) {
		this.name = name;
	}

	@Override
	public void run() {
		LocalDateTime ldt = LocalDateTime.now();
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss.SSS");
		System.out.println(ldt.format(formatter) + " Started thread " + name);
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		ldt = LocalDateTime.now();
		System.out.println(ldt.format(formatter) + "  Ended thread " + name);
	}

}
								

Now let's rewrite our application class to use two scheduled thread pools. We will do the following:
  • Use executorService.schedule to schedule a one-off runnable task to the thread pool. After a specified delay this task will be executed using a thread from the scheduled thread pool.
  • Use executorService.scheduleAtFixedRate to schedule a repeating task with the thread pool. Given some timeunit and some delay d, the same runnable will be executed every d timeunits after the start time of the previous execution.
  • Use executorService.scheduleWithFixedDelay to schedule a repeating task with the thread pool. Given some timeunit and some delay d, the same runnable will be executed every d timeunits after the end time of the last execution.
Notice the difference between fixed rate and fixed delay. With fixed rate, if a task takes longer than the delay interval, the next execution will start late, but will not concurrently execute.

package com.example.demo;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MultiThreadedApplication {

	public static void main(String[] args) {
		
		ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
		
		System.out.println("");
		System.out.println("First scheduled threat pool will run for 30 seconds");
		
		executorService.schedule(new RunnableObject("my one-off delayed task "), 10, TimeUnit.SECONDS);
		executorService.scheduleAtFixedRate(new RunnableObject("my fixed rate task"), 15, 5, TimeUnit.SECONDS);
		
		SpringApplication.run(MultiThreadedApplication.class, args);
		
		
		try {
			Thread.sleep(30000);  // let run 30 seconds
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		executorService.shutdown();
		
		while (!executorService.isTerminated())	{
			// wait
		}
		System.out.println("All threads from first thread pool completed successfully");
		
		
		System.out.println("");
		System.out.println("Second scheduled threat pool will run for 30 seconds");

		executorService = Executors.newScheduledThreadPool(10);
		executorService.scheduleWithFixedDelay(new RunnableObject("my fixed delay task"), 5, 5, TimeUnit.SECONDS);
		
		
		try {
			Thread.sleep(30000);  // let run 30 seconds
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		executorService.shutdown();
		
		while (!executorService.isTerminated())	{
			// wait
		}
		System.out.println("All threads from second thread pool completed successfully");


	}

}

					


Multithreading In SpringBoot

Now let's get to actually doing multithreading in SpringBoot! In order to make methods that run in a separate thread the "SpringBoot way", we will use @Async annotations on those methods. But first, we must make an async config class to define the thread pool those methods will use. This config must have the @EnableAsync annotation.
Create a class like this:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name="taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("poolThread-");
        executor.initialize();
        return executor;
    }
}
				

Since core pool size and max pool size are the same here, the thread pool will always have two threads, along with a queue of 100. Note we can access this bean programmatically to check the status of our thread pool, but first let's write some multithreaded code!
We will create an async method that will simulate calling an API to get a stock price. We will simulate an API response time of 1 second by adding in a Thread.sleep(1000) or one second. We will assume that if we make more than one simultaneous call to the API, it does not change the response time. Then we will create a method that will make three calls to this API simultaneously, which should result in a response coming back in less than three seconds.
Let's start by adding a class that will make run the code in a one of the threads in the thread pool. As usual, Spring is smart and if no executor is provided as an argument to the Async annotation it finds one automatically from the registered beans. In this case it finds the bean we just made called "taskExecutor."

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class StockPriceService {

    Logger logger = LoggerFactory.getLogger(StockPriceService.class);

    @Async
    public  CompletableFuture<Double>  getStockPrice(String stockName) throws InterruptedException {
        logger.info("Starting: getStockPrice for company {} with thread {}", stockName, Thread.currentThread().getName());
        Thread.sleep(1000);
        Double price;
        switch (stockName) {
            case "company1":
                price = 50.0;
                break;
            case "company2":
                price = 20.0;
                break;
            case "company3":
                price= 15.0;
                break;
            default:
                price = null;

        }
        logger.info("Complete: getStockPrice for company {} with thread {}", stockName, Thread.currentThread().getName());

        return CompletableFuture.completedFuture(price);
    }
}
				
One thing to note is that you cannot call async methods from the same class. So, we will call the above method from a rest controller.
Let's make a GET call for this service that will simply trigger a lookup of the three stock prices. Add the below class.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

@RestController
public class StockPriceController {

    @Autowired
    StockPriceService stockPriceService;

    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;

    Logger logger = LoggerFactory.getLogger(StockPriceController.class);

    @GetMapping(value="/getAllStockPrices", produces="application/json")
    public ResponseEntity getAllStockPrices() {

        List<Double> stockPrices  = getAllThreeStockPrices();
        List<String> stockPriceStrings = stockPrices.stream().map(Object::toString).collect(Collectors.toList());

        return ResponseEntity.ok().body(String.join(",", stockPriceStrings));
    }

    public List<Double> getAllThreeStockPrices() {
        List<Double> stockPriceList = new ArrayList<>();
        CompletableFuture<Double> cf1, cf2, cf3;
        try {
            logger.info("Calling async getStockPrice for company1, active count: {}, Pool size: {}, Queue Size: {}", taskExecutor.getActiveCount(), taskExecutor.getPoolSize(), taskExecutor.getThreadPoolExecutor().getQueue().size());
            cf1 = stockPriceService.getStockPrice("company1");
            logger.info("Calling async getStockPrice for company2, active count: {}, Pool size: {}, Queue Size: {}", taskExecutor.getActiveCount(), taskExecutor.getPoolSize(), taskExecutor.getThreadPoolExecutor().getQueue().size());

            cf2 = stockPriceService.getStockPrice("company2");
            logger.info("Calling async getStockPrice for company3, active count: {}, Pool size: {}, Queue Size: {}", taskExecutor.getActiveCount(), taskExecutor.getPoolSize(), taskExecutor.getThreadPoolExecutor().getQueue().size());

            cf3 = stockPriceService.getStockPrice("company3");
            logger.info("After three calls to async getStockPrice, active count: {}, Pool size: {}, Queue Size: {}", taskExecutor.getActiveCount(), taskExecutor.getPoolSize(), taskExecutor.getThreadPoolExecutor().getQueue().size());

            // Monitor the threads as they complete.
            int sleeptime = 0;
            for(int i = 0; i < 20; i++) {
                Thread.sleep(100);
                sleeptime=(i+1)*100;
                logger.info("After {} milliseconds, active count: {}, Pool size: {}, Queue Size: {}", sleeptime, taskExecutor.getActiveCount(), taskExecutor.getPoolSize(), taskExecutor.getThreadPoolExecutor().getQueue().size());
            }
            stockPriceList.add(cf1.get());
            stockPriceList.add(cf2.get());
            stockPriceList.add(cf3.get());
        }
        catch (Exception e) {
            System.out.println("error " + e);
        }

        return stockPriceList;

    }
}


If you don't have a port defined in application properties, go ahead and make an application properties file with a port number.

server.port=8082
Go ahead and make your GET call to http://localhost:8082/getAllStockPrices from Postman. You should see something like the below. Scroll on - Let's break it down.

2021-08-08 18:18:30.486  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company1, active count: 0, Pool size: 0, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company2, active count: 1, Pool size: 1, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company3, active count: 2, Pool size: 2, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After three calls to async getStockPrice, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.500  INFO 15596 --- [    threadNum-1] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company1 with thread threadNum-1
2021-08-08 18:18:30.500  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company2 with thread threadNum-2
2021-08-08 18:18:30.600  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 100 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.709  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 200 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.817  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 300 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.926  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 400 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.035  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 500 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.143  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 600 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.252  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 700 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.361  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 800 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.473  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 900 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-1] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company1 with thread threadNum-1
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company2 with thread threadNum-2
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company3 with thread threadNum-2
2021-08-08 18:18:31.580  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1000 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.687  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1100 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.796  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1200 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.904  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1300 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.013  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1400 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.123  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1500 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.233  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1600 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.344  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1700 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.454  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1800 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.517  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company3 with thread threadNum-2
2021-08-08 18:18:32.564  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1900 milliseconds, active count: 0, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.675  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 2000 milliseconds, active count: 0, Pool size: 2, Queue Size: 0
2021-08-08 18:18:30.486  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company1, active count: 0, Pool size: 0, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company2, active count: 1, Pool size: 1, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : Calling async getStockPrice for company3, active count: 2, Pool size: 2, Queue Size: 0
2021-08-08 18:18:30.493  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After three calls to async getStockPrice, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.500  INFO 15596 --- [    threadNum-1] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company1 with thread threadNum-1
2021-08-08 18:18:30.500  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company2 with thread threadNum-2
2021-08-08 18:18:30.600  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 100 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.709  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 200 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.817  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 300 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:30.926  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 400 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.035  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 500 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.143  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 600 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.252  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 700 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.361  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 800 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.473  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 900 milliseconds, active count: 2, Pool size: 2, Queue Size: 1
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-1] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company1 with thread threadNum-1
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company2 with thread threadNum-2
2021-08-08 18:18:31.503  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Starting: getStockPrice for company company3 with thread threadNum-2
2021-08-08 18:18:31.580  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1000 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.687  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1100 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.796  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1200 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:31.904  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1300 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.013  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1400 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.123  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1500 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.233  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1600 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.344  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1700 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.454  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1800 milliseconds, active count: 1, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.517  INFO 15596 --- [    threadNum-2] c.j.e.api.service.StockPriceService      : Complete: getStockPrice for company company3 with thread threadNum-2
2021-08-08 18:18:32.564  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 1900 milliseconds, active count: 0, Pool size: 2, Queue Size: 0
2021-08-08 18:18:32.675  INFO 15596 --- [nio-8082-exec-2] c.j.e.a.controller.StockPriceController  : After 2000 milliseconds, active count: 0, Pool size: 2, Queue Size: 0

So what is happening above?
  1. Active threads increase to 1 after async method call 1
  2. Active threads increase to 2 after async method call 2
  3. Threads are all in use, so active threads remains at 2 and queue size increases to 1 after async method call 3
  4. After about a second, threadNum-1 and threadNum-2 finish. In this case, the item from the queue moves to threadNum-2.
  5. Now there's only 1 active thread and 0 items in the queue.
  6. After about another second, threadNum-2 completes. The number of active threads is now 0.


That's all for this tutorial. For further reading, look into the fancy things that can be done with CompleteableFutures, such as the join and thenApply methods.

Troubleshooting


Seeing the above? Follow these steps.