# Enabling Virtual Thread in Kotlin + Spring Boot

### Overview

* Starting with **Java 19**, the concept of `Virtual Thread` has been newly added as a **Preview Feature**, and it is scheduled to be included as an official feature from **Java 21 LTS**. While the traditional **Platform Thread** directly maps to the threads of the **Operating System(OS)**, **Virtual Thread** operates as a lightweight virtual thread abstracted by the **Java Virtual Machine(JVM)**, consuming significantly less memory. Virtual threads are automatically managed by the JVM's scheduler, allowing developers to focus more on business logic while enjoying performance benefits.
    
* **Spring Boot 3** and **Spring Framework 6** officially support **Virtual Thread**s. This article summarizes how to replace platform threads that handle **Spring Web MVC** requests, **@Async**, and coroutine executions in Spring Boot-based projects with virtual threads. (All the content below has been verified in a production environment.)
    

### Virtual Thread Features

* **Java 19/20** started offering `Virtual Thread` as a Preview Feature, and as a full feature in **Java 21**, which is the **LTS** version.
    
* Traditional **Platform Thread**s, which directly wrap operating system threads, could not be used for the duration of blocking caused by **IO** or computing, such as network and database operations. **Virtual Thread**s, on the other hand, are much lighter as logical units managed by the **JVM**, isolated from physical OS threads. Crucially, when blocking occurs, the **OS** thread being used can be utilized by another Virtual Thread, significantly improving concurrency processing. This allows developers to achieve dramatic performance improvements in traditional sequential programming without significant changes, unlike the **Reactive** approach. (This represents a major innovation in the **JVM** community after many years.)
    
* The **Java** community has made meticulous efforts to maintain backward compatibility with the introduction of **Virtual Thread**s. `Executors.newVirtualThreadPerTaskExecutor()` can be used to easily create an **Executor** object. Integrating this with servlet containers and Kotlin coroutines allows for the immediate use of **Virtual Thread**s without changing existing code.
    
* There are two ways to use **Virtual Thread**. **Java 19/20** requires you to add the option `--release 19 --enable-preview` at project build time and `--enable-preview` at run time. **Java 21**, on the other hand, does not need to add this option.
    

### Installing OpenJDK 21

* Install `OpenJDK 21` in the development environment as follows. (For convenience, `SDKMAN` was used.)
    

```bash
# Install SDKMAN in Linux, macOS
$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"

# Install Amazon Corretto 21 and set as the default JDK
$ sdk i java 21.0.1-amzn
$ sdk default java 21.0.1-amzn

$ sdk current java
Using java version 21.0.1-amzn

# Check installed version
$ java --version
openjdk 21.0.1 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-21.0.1.12.1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.1.12.1 (build 21.0.1+12-LTS, mixed mode, sharing)
```

### Environment Variables

* Add the following to your project or operating environment variables.
    

```bash
# Activate Virtual Threads in Spring Boot
SPRING_THREADS_VIRTUAL_ENABLED=true
```

### Activating JDK 21 in IntelliJ IDEA

* In **IntelliJ IDEA**, activate the previously installed **JDK 21** at the project level as follows:
    

```bash
Settings → Project Structure
→ SDK: select [corretto-21]
→ Language Level: select [21 (Preview) - String templates, unnamed classes and instance main methods etc.]
```

### build.gradle.kts

* Add the following content to **build.gradle.kts** in the project root to activate **JDK 21** at the build level.
    

```kotlin
// BEFORE: java.sourceCompatibility = JavaVersion.VERSION_17
java.sourceCompatibility = JavaVersion.VERSION_21

tasks.withType<KotlinCompile> {
    kotlinOptions {
        // BEFORE: freeCompilerArgs = listOf("-Xjsr305=strict")
        // BEFORE: jvmTarget = "17"
        // AFTER: Only JDK 19/20, Add --release 19 --enable-preview option at the compile stage
        freeCompilerArgs = listOf("-Xjsr305=strict --release 21")
        jvmTarget = "21"
    }
}

// AFTER: Only JDK 19/20, Add --enable-preview option at runtime
tasks.withType<JavaExec> {
    jvmArgs = listOf("--enable-preview")
}
```

### Switching HTTP Request Handling to Virtual Threads

* In the **Java** ecosystem, servlet containers handle requests through physical platform threads, which are OS threads wrapped for each unit request. The following code allows you to instruct **Apache Tomcat**, the servlet implementation embedded in **Spring Boot**, to process all requests using virtual threads instead of platform threads.
    

```kotlin
import org.apache.coyote.ProtocolHandler
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.concurrent.Executors

@Configuration
class TomcatConfig {

    @Bean
    fun protocolHandlerVirtualThreadExecutorCustomizer(): TomcatProtocolHandlerCustomizer<*>? {
        return TomcatProtocolHandlerCustomizer<ProtocolHandler> { protocolHandler: ProtocolHandler ->
            protocolHandler.executor = Executors.newVirtualThreadPerTaskExecutor()
        }
    }
}
```

### Switching Asynchronous Execution to Virtual Threads

* One of the most convenient methods for executing asynchronous logic in the **Spring Boot** ecosystem, `@Async`, traditionally runs on threads allocated by **AsyncTaskExecutor** implementations based on a platform thread pool. This can be switched to virtual threads as follows.
    

```kotlin
import org.slf4j.MDC
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.task.AsyncTaskExecutor
import org.springframework.core.task.TaskDecorator
import org.springframework.core.task.support.TaskExecutorAdapter
import org.springframework.scheduling.annotation.EnableAsync
import java.util.concurrent.Executors

@Configuration
@EnableAsync
class AsyncConfig {

    @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    fun asyncTaskExecutor(): AsyncTaskExecutor {

        val taskExecutor = TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor())
        taskExecutor.setTaskDecorator(LoggingTaskDecorator())

        return taskExecutor
    }
}

class LoggingTaskDecorator : TaskDecorator {

    override fun decorate(task: Runnable): Runnable {

        val callerThreadContext = MDC.getCopyOfContextMap()

        return Runnable {
            callerThreadContext?.let {
                MDC.setContextMap(it)
            }
            task.run()
        }
    }
}
```

* In Spring Beans, you can run asynchronous logic with **Virtual Thread** as shown below.
    

```kotlin
import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Service

@Service
class FooService {
	
	@Async(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
	fun doSomething() {
		// doSomething
	}
}
```

### Switching Scheduler Execution to Virtual Threads

* In the **Spring Boot** ecosystem, `@Scheduled` tasks, which are executed at specific times based on defined rules, traditionally run on threads allocated by **ThreadPoolTaskExecutor** implementations based on a platform thread pool. This can be switched to virtual threads as follows.
    

```kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.TaskScheduler
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler
import java.util.concurrent.Executors

@Configuration
@EnableScheduling
class SchedulingConfig {

    @Bean
    fun taskScheduler(): TaskScheduler {

        // [Option 1] Activate Virtual Threads in JDK 21
        return SimpleAsyncTaskScheduler().apply {
            this.setVirtualThreads(true)
            this.setTaskTerminationTimeout(30 * 1000)
        }

        // [Option 2] Activate Virtual Threads in JDK 19/20
        return ConcurrentTaskScheduler(
            Executors.newScheduledThreadPool(0, Thread.ofVirtual().factory())
        )
    }
}
```

### Switching Kotlin Coroutine Execution to Virtual Threads

* **Kotlin** has long provided developers with a similar **suspend** function through coroutines, even before the advent of **Virtual Thread**s. By writing the code below and using **Dispatchers.LOOM**, which applies virtual threads instead of the traditionally used **Dispatchers.IO**, coroutines can be executed on virtual threads.
    

```kotlin
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors

val Dispatchers.LOOM: CoroutineDispatcher
    get() = Executors.newVirtualThreadPerTaskExecutor().asCoroutineDispatcher()
```

### Creating a Dockerfile for Amazon ECS Container Deployment

* To launch a container with **JVM 21** in an **Amazon ECS** environment, a `Dockerfile` can be written as follows. (At the time of writing this article, there wasn't a suitable **OpenJDK 21**\-based Docker base image available, so an example is provided where **OpenJDK 21** is installed directly. This method has been verified in a production-level environment.)
    

```bash
# To avoid rate limit restrictions, AWS Public ECR is used instead of DockerHub, and a base image verified for compatibility with Amazon ECS is utilized.
FROM public.ecr.aws/ews-network/amazoncorretto:17-debian
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
      HTTP_PROXY=http:... \
      HTTPS_PROXY=http:...
EXPOSE 8080
USER root
# Install OpenJDK 21, you can use a base image based on 20 according to your environment.
RUN apt update -y
RUN apt install wget gnupg -y
RUN update-ca-certificates
RUN wget https://apt.corretto.aws/corretto.key
RUN apt-key add corretto.key
RUN echo 'deb https://apt.corretto.aws stable main' | tee /etc/apt/sources.list.d/corretto.list
RUN apt-get update -y
RUN apt-get install java-21-amazon-corretto-jdk -y
# Assuming the path to the .jar file built via Gradle settings is build/libs/app.jar, modify it according to your environment.
COPY build/libs/app.jar /app.jar
COPY buildspec/entrypoint.sh /
ENTRYPOINT ["sh", "/entrypoint.sh"]
```

* Write `entrypoint.sh` as follows. The key point is specifying the `--enable-preview` option at runtime.
    

```bash
#!/bin/sh
export ECS_INSTANCE_IP_TASK=$(curl --retry 5 -connect-timeout 3 -s ${ECS_CONTAINER_METADATA_URI})
export ECS_INSTANCE_HOSTNAME=$(cat /proc/sys/kernel/hostname)
export ECS_INSTANCE_IP_ADDRESS=$(echo ${ECS_INSTANCE_IP_TASK} | jq -r '.Networks[0] | .IPv4Addresses[0]')
echo "${ECS_INSTANCE_IP_ADDRESS} ${ECS_INSTANCE_HOSTNAME}" | sudo tee -a /etc/hosts
exec java ${JAVA_OPTS} -server -XX:+UseZGC -XX:+ZGenerational --enable-preview -jar /app.jar
```

* For integration with `AWS CodePipeline`, a `buildspec.xml` can be written as follows. (The **{region}, {repository-uri}, {image-name}, {container-name}** parameters should be appropriately modified for your project environment.)
    

```bash
version: 0.2

phases:
  install:
    runtime-versions:
      java: corretto21
    run-as: root
    commands:
      - update-ca-trust
      - javac --version
  pre_build:
    commands:
      - REGION={region}
      - REPOSITORY_URI={repository-uri}
      - IMAGE_NAME={image-name}
      - IMAGE_TAG=latest
      - DEPLOY_TAG=dev
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - BUILD_TAG=${COMMIT_HASH:=dev}
      - CONTAINER_NAME={container-name}
      - DOCKERFILE_PATH=buildspec/Dockerfile
      - echo Logging in to Amazon ECR...
      - aws --version
      - aws ecr get-login-password --region $REGION | docker login -u AWS --password-stdin $REPOSITORY_URI
  build:
    commands:
      - echo Building the Docker image...
      - chmod +x ./gradlew
      - ./gradlew build -x test
      - docker build -f $DOCKERFILE_PATH -t $IMAGE_NAME .
      - docker tag $IMAGE_NAME:$IMAGE_TAG $REPOSITORY_URI/$IMAGE_NAME:$DEPLOY_TAG
  post_build:
    commands:
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI/$IMAGE_NAME:$DEPLOY_TAG
      - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $REPOSITORY_URI/$IMAGE_NAME:$DEPLOY_TAG > imagedefinitions.json
      - cat imagedefinitions.json

cache:
  paths:
    - '/root/.m2/**/*'
    - '/root/.gradle/caches/**/*'

artifacts:
  files:
    - imagedefinitions.json
```

### Reference Articles

* [Virtual Threads: New Foundations for High-Scale Java Applications](https://www.infoq.com/articles/java-virtual-threads/)
    
* [Embracing Virtual Threads](https://spring.io/blog/2022/10/11/embracing-virtual-threads)
    
* [Running Kotlin coroutines on Project Loom's virtual threads](https://kt.academy/article/dispatcher-loom)
    
* [Boost Your Application’s Performance with Virtual Threads in Java and Spring: Exploring Project Loom](https://medium.com/@knowledge.cafe/spring-boot-virtual-threads-52e28bb0ca5)
