What Happens to the Thread Count When Running a JAR on Linux?
- JVM threads are mapped directly to OS threads.
- This is known as the Native Thread Implementation or the 1:1 Threading Model.
Thread Pool in Spring Boot
By default, Spring Boot creates a thread pool when using an embedded Tomcat server.
The default configuration allows for a maximum of 200 threads.
Each incoming request is handled by a worker thread from this pool.
Q1. If I spin up a container using a Docker image, does that container get assigned processes and threads from the host OS?
1. Relationship Between Containers and Processes
A Docker container runs as an isolated group of processes on the host OS.
It leverages Linux namespaces and cgroups to isolate processes and limit resource usage.
In reality, it shares the host OS kernel.
2. In the Case of Spring Boot Applications
The JVM running inside the container is also a process on the host OS.
JVM threads are still mapped to the host OS's native threads.
However, they are subject to the container's resource constraints (CPU, Memory, etc.).
3. Verification Commands
Bash
# Check container processes from the host OS
docker top [container-id]
# Check processes inside the container
docker exec [container-id] ps -ef
# Check specific threads
docker exec [container-id] ps -eLf
1. Container Structure
Each container includes its own application, libraries, and runtime.
Containers remain isolated from one another.
2. Namespace Isolation
PID Namespace: Isolates Process IDs.
- A process may have PID 1 inside the container but appear as PID 1234 on the host system.
Network Namespace: Isolates the network stack.
Provides each container with an independent network stack (interfaces, IP addresses, routing tables, port numbers, iptables rules).
When a container is created, Docker generates a veth (virtual ethernet) pair.
One end resides in the host's network namespace, while the other (eth0) resides in the container's.
These interfaces act like a pipe to forward traffic.
Port Mapping
-p 8080:80
Mount Namespace: Isolates filesystem mount points.
User Namespace: Isolates User and Group IDs.
3. Resource Control (cgroups)
- Manages CPU, Memory, and I/O usage.
4. Kernel Sharing
- All containers share the same host OS kernel and access system resources through it.
Q2. In a Kotlin Spring Boot environment, are the threads in the Dispatchers pool used by Coroutines also OS threads?
1. Dispatcher Thread Pools
Dispatchers.Default: Uses a thread pool proportional to the number of CPU cores (Minimum 2, Maximum: CPU cores + 1).
Dispatchers.IO: Uses a shared thread pool (defaults to up to 64 threads).
All of these are indeed native OS threads.
2. Coroutines vs. Threads
While Coroutines are called "lightweight threads," they are actually units of work executed on top of threads.
Multiple coroutines can run on a single thread (M:N mapping).
Switching between coroutines is significantly "lighter" than switching between threads.
Q3. When a Coroutine Scope is executed, is a specific thread type assigned? And is switching between coroutines really cheaper than thread context switching?
1. Coroutine Scope and Dispatchers
Kotlin
// Determine which thread pool to use via the Dispatcher
CoroutineScope(Dispatchers.Default).launch {
// All coroutines in this scope use the Default thread pool
// Switching between coroutines (e.g., during suspension) is extremely lightweight
val result1 = async { heavyComputation() }
val result2 = async { anotherComputation() }
}
2. Context Switching Cost Comparison
Thread Context Switch: Occurs at the OS level (Expensive).
- Requires saving/loading CPU register states: Program Counter (PC), Stack Pointer (SP), General-purpose registers, and Status registers.
Thread Switching Process:
Thread A is running -> Save Thread A's register values to memory (in the PCB).
Load Thread B's register values from memory.
Start Thread B execution.
What is a PCB (Process Control Block)?
A metadata structure maintained by the OS to manage each process.
Management Info: PID, Status, Priority, PC, CPU Registers, Scheduling info.
Memory Info: Allocation, Page/Segment tables.
File/IO Info: Open files, File descriptors, I/O devices.
Memory Map Switching: Switching the virtual memory address space.
TLB Flushing: Increases page table walks as the translation cache becomes invalid.
Increased Cache Misses/Page Faults: Since the new process needs different data, memory access latency increases.
CPU Cache Invalidation: Data in the L1/L2/L3 caches may no longer be valid for the new thread, leading to cache misses.
Coroutine Switching:
Occurs within the same thread.
Only saves execution state and stack information.
Extremely lightweight with minimal memory access.
Kotlin
// Conceptual structure of Coroutine Suspension (Continuation)
fun example(continuation: ExampleContinuation) {
when(continuation.label) {
0 -> {
continuation.x = 10
continuation.label = 1
delay(1000, continuation)
}
1 -> {
val x = continuation.x // Restore state
println(x)
continuation.label = 2
delay(2000, continuation)
}
2 -> {
val x = continuation.x
println(x + 1)
}
}
}
Q4. If I set the Spring Boot Max Thread Pool to 200, are Async and Coroutine threads created separately?
Yes, they use independent thread pools:
Tomcat Thread Pool:
server.tomcat.threads.max=200(For HTTP requests).Spring @Async Pool: Configured separately via
ThreadPoolTaskExecutor.Coroutine Dispatcher Pool:
DefaultandIOdispatchers maintain their own pools.
Total Thread Count = Tomcat Threads + @Async Threads + Coroutine Threads.
In a Docker environment, you must be cautious as the sum of these pools can lead to heavy resource contention.
Q5. If I run a JAR in Docker with Max Threads at 200, 2 Async threads, and default Coroutine settings, how are Coroutine threads calculated?
Tomcat: Max 200.
Async: 2.
Dispatchers.Default:
min(Core Count * 2, 128).Dispatchers.IO: Max 64.
For an 8-core system:
$200 (Tomcat) + 2 (Async) + 16 (Default) + 64 (IO) = 282$ potential threads.
Note: Threads are created/destroyed dynamically based on demand; they aren't all allocated at startup.
Q6. What happens to resources if I spin up a second identical container on the same host?
Container 1: Up to 282 threads.
Container 2: Up to 282 threads.
Total: Up to 564 threads.
Critical Point: Both containers share the same host CPU and memory. Without limits, they will compete for resources, leading to resource contention and performance degradation. It is best practice to define limits:
YAML
# docker-compose.yml example
services:
app1:
cpus: '4'
pids_limit: 150
app2:
cpus: '4'
pids_limit: 150
Q7. Maximize Performance: Large Server with Many Containers vs. Multiple Small Servers?
Strategy | Pros |
Many Containers on One Server | Efficient resource sharing, simpler management, lower cost, fast inter-container communication. |
Distributed over Small Servers | Better fault isolation, easier individual scaling, less resource contention, hardware redundancy. |
Verdict: It depends. Complementary services should be grouped, while high-load/mission-critical services should be isolated.
Q8. In K8s (1 Master, 3 Nodes), does scaling out mean adding more containers to the same server?
Scaling out in Kubernetes follows two paths:
Sufficient Node Resources: The K8s Scheduler places new Pods (containers) on existing nodes. Multiple containers will run on one server.
Insufficient Node Resources: Pods enter a Pending state. You must add physical/virtual nodes. In cloud environments, Cluster Autoscaler can automate this.
Pro-tip: Always define resources.requests and limits to make scaling predictable.

