├── .gitignore ├── pom.xml └── src └── main ├── java └── com │ └── daluga │ └── thread │ ├── MultiThreadedExampleApplication.java │ └── WorkerThread.java └── resources ├── application.properties └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # VSCode File 12 | .vscode/* 13 | 14 | # Mac 15 | .DS_Store 16 | 17 | # Maven 18 | log/ 19 | target/ 20 | .mvn 21 | mvnw* 22 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.daluga 7 | spring-boot-multithreaded-example 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-boot-multithreaded-example 12 | Spring Boot Multithreaded Example 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.7.2 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 17 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/com/daluga/thread/MultiThreadedExampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.daluga.thread; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.ApplicationContext; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.concurrent.*; 15 | 16 | @SpringBootApplication 17 | public class MultiThreadedExampleApplication implements CommandLineRunner { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(MultiThreadedExampleApplication.class); 20 | 21 | @Autowired 22 | private ApplicationContext context; 23 | 24 | @Value("${number.of.requests}") 25 | private int numberOfRequests; 26 | 27 | @Value("${number.of.threads}") 28 | private int numberOfThreads; 29 | 30 | public static void main(String[] args) { 31 | SpringApplication.run(MultiThreadedExampleApplication.class, args); 32 | } 33 | 34 | @Override 35 | public void run(String... strings) throws Exception { 36 | LOGGER.debug("Spring Boot multithreaded example has started...."); 37 | LOGGER.debug("Number of requests: " + numberOfRequests); 38 | LOGGER.debug("Number of threads: " + numberOfThreads); 39 | LOGGER.debug("Number of processors: " + Runtime.getRuntime().availableProcessors()); 40 | 41 | ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); 42 | 43 | // TODO: This does not seem to work and need to research why. 44 | // A java program can't terminate or exit while a normal thread is executing. So, left over threads 45 | // waiting for a never-satisfied event can cause problems. However, if you have blocks that need 46 | // to be executed (like a finally block to clean up resources) then you should not set daemon threads 47 | // to true. It all depends on the context of your solution. 48 | // ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads, threadFactory -> { 49 | // Thread t = new Thread(); 50 | // t.setDaemon(true); 51 | // return t; 52 | // }); 53 | 54 | List tasks = new ArrayList<>(numberOfRequests); 55 | 56 | // -------------------------------------------------------------------------------------------------------------- 57 | // Notes for multithreading separate worker tasks. 58 | // -------------------------------------------------------------------------------------------------------------- 59 | // 1. You need to create the WorkerThread from the spring context so that bean will be properly injected with 60 | // it's dependencies. You cannot use this: WorkerThread wt = new WorkerThread(); 61 | // 2. The WorkerThread class must implement the Callable interface so that it can be executed in a separate 62 | // thread. 63 | // 3. The WorkerThread class needs to have a scope of prototype so that it is not a singleton. The singleton 64 | // scope is the default scope for a Spring bean. As a result, this class must have this annotation: 65 | // @Scope("prototype") 66 | // -------------------------------------------------------------------------------------------------------------- 67 | 68 | for (int i = 0; i < numberOfRequests; i++) { 69 | WorkerThread wt = context.getBean(WorkerThread.class, String.valueOf(i)); 70 | tasks.add(wt); 71 | } 72 | 73 | // The problem with this approach is that this blocks until all threads have completed. As a result, you need 74 | // to wait before all threads have executed before you see any of the results in the get() method. 75 | List> futures = executorService.invokeAll(tasks); 76 | 77 | for (Future future : futures) { 78 | String result = future.get(10000, TimeUnit.MILLISECONDS); 79 | LOGGER.debug("Thread reply results [" + result + "]"); 80 | } 81 | 82 | executorService.shutdown(); 83 | 84 | LOGGER.debug("Spring Boot multithreaded example has ended...."); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/daluga/thread/WorkerThread.java: -------------------------------------------------------------------------------- 1 | package com.daluga.thread; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.annotation.Scope; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.UUID; 9 | import java.util.concurrent.Callable; 10 | 11 | // We need a scope of prototype so that a new instance is created each time. The default scope is singleton so you would always get the same instance 12 | // if we did not specify a scope of prototype. 13 | 14 | @Component 15 | @Scope("prototype") 16 | public class WorkerThread implements Callable { 17 | 18 | private static final Logger LOGGER = LoggerFactory.getLogger(WorkerThread.class); 19 | 20 | private String request; 21 | 22 | public WorkerThread(String request) { 23 | this.request = request; 24 | } 25 | 26 | @Override 27 | public String call() throws Exception { 28 | LOGGER.debug("Thread started [" + request + "]"); 29 | return doWork(); 30 | } 31 | 32 | private String doWork() { 33 | try { 34 | Thread.sleep(5000); 35 | } catch (InterruptedException e) { 36 | LOGGER.error("An unexpected interrupt exception occurred!", e); 37 | } 38 | 39 | return "Request [" + request + "] " + createUUID(); 40 | } 41 | 42 | private String createUUID() { 43 | UUID id = UUID.randomUUID(); 44 | return id.toString(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | number.of.requests=100 2 | number.of.threads=5 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.out 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - [%thread] - %m%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | --------------------------------------------------------------------------------