Multi-threading in Spring
Thread Creation
- Spring use
org.springframework.core.task.TaskExecutorto create and manage threads. - Use
ThreadLocalfor thread-specific data to avoid shared state conflicts.
@Bean(name = "apiTaskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8); // Base number of threads
executor.setMaxPoolSize(16); // Maximum threads allowed
executor.setQueueCapacity(100); // Task queue size
executor.setThreadNamePrefix("AsyncThread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setTaskDecorator(runnable -> {
String userId = userContext.get();
return () -> {
try {
userContext.set(userId);
runnable.run();
} finally {
userContext.remove();
}
};
});
executor.initialize();
return executor;
}
private final ThreadLocal<String> userContext = new ThreadLocal<>();
userContext.set(userId);
userContext.get();
userContext.remove();
Thread Management
- Create distinct thread pools for different types of tasks.
- One pool for database operations, another for external API calls.
- Use
CompletableFuturefor asynchronous tasks.@Autowired
@Qualifier("apiTaskExecutor")
private TaskExecutor apiTaskExecutor;
public CompletableFuture<String> fetchDataAsync(String url) {
return CompletableFuture.supplyAsync(() -> {
// Simulate external API call
return callExternalApi(url);
}, apiTaskExecutor);
}
Asynchronous Tasks
- Asynchronous processing is ideal for long-running or I/O-bound tasks (e.g. calling external APIs)
- Use
@Transactionalin async methods for database operations.@Async("dbTaskExecutor")
@Transactional
public CompletableFuture<Void> saveDataAsync(Data data) {
dataRepository.save(data);
return CompletableFuture.completedFuture(null);
}
Locking
-
Use
synchronizedorReentrantLock.public synchronized void updateSharedResource() {
// Critical section
}
private final ReentrantLock lock = new ReentrantLock();
public void updateResource() {
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
} -
Use
ConcurrentHashMap,CopyOnWriteArrayList, orBlockingQueuefor simple shared stage. -
Use optimistic locking with
@Versionin JPA.@Entity
public class Product {
@Id
private Long id;
@Version
private Long version;
// Other fields
} -
Use pessimistic locking with
LockModeType.@Query("SELECT p FROM Product p WHERE p.id = :id")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Product lockProduct(@Param("id") Long id); -
Use Redis to implement distributed locks with
INCR,EXPIRE, andDEL
Thread Local
- When a thread terminates, its
ThreadLocalMapis also destroyed. However, theThreadLocalobject itself is not immediately garbage collected until there are no other references to it. - Therefore, when using
ThreadLocal, you need to be cautious. If theremove()method is not explicitly called, or if theThreadLocalvariables are not properly cleaned up when the thread ends, it may lead to memory leaks. This is because theThreadLocalMapwill continue to hold references to theThreadLocalvariables, even if they are no longer referenced elsewhere.
TaskDecorator
The 'correlation-id' Challenge

- A TaskDecorator is simply a Callable interface you can implement and enhance your Executor with.
- We can copy of the correlation-id from the parent thread
- In this way, any thread that is executed by our executor will be supplied with the parent correlation-id property.
- https://medium.com/att-israel/dont-lose-your-thread-manage-and-decorate-your-concurrent-threads-391cf34e6bc6.