← Назад к вопросам

Сколько стеков одновременно в Java приложении?

2.2 Middle🔥 71 комментариев
#JVM и управление памятью#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стеки в Java: threads и memory model

Каждый поток имеет свой стек (stack). Кол-во стеков в приложении = кол-во потоков, которые работают одновременно. Давайте разберём, как это работает и сколько стеков нужно приложению.

Основа: Thread = Stack

public class StackExample {
    public static void main(String[] args) {
        // Main thread (создан JVM) - имеет свой stack
        System.out.println(Thread.currentThread().getName());
        // Output: main
        
        // Создаём новый thread
        Thread thread1 = new Thread(() -> {
            // Этот thread имеет СВОЙ stack
            doSomething();
        });
        thread1.start();  // ← Новый stack создан для thread1
        
        // Теперь работают 2 стека одновременно:
        // - main thread stack
        // - thread1 stack
    }
    
    static void doSomething() {
        int[] array = new int[1000];  // На stack'е thread1, не main
        callAnother();
    }
    
    static void callAnother() {
        String str = "hello";  // На stack'е текущего потока
    }
}

Визуально: Memory Layout

Java Heap (всем потокам):
┌─────────────────────────────────┐
│ Objects, arrays                 │
│ (shared between all threads)     │
│                                 │
│ User obj, Order obj, String obj │
└─────────────────────────────────┘

Thread Stacks (каждому потоку свой):

Main Thread Stack:       Thread-1 Stack:      Thread-2 Stack:
┌──────────────────┐    ┌──────────────────┐ ┌──────────────┐
│ Local vars       │    │ Local vars       │ │ Local vars   │
│ int x = 5        │    │ String name      │ │ User user    │
│ User user        │    │ int count = 100  │ │              │
│ (reference)      │    │ (reference)      │ │              │
├──────────────────┤    ├──────────────────┤ ├──────────────┤
│ Method calls     │    │ Method calls     │ │ Method calls │
│ main()           │    │ doWork()         │ │ process()    │
│ processData()    │    │ helper()         │ │              │
│ helper()         │    │                  │ │              │
├──────────────────┤    ├──────────────────┤ ├──────────────┤
│ (Stack)         │    │ (Stack)          │ │ (Stack)      │
│ 1 MB            │    │ 1 MB             │ │ 1 MB         │
└──────────────────┘    └──────────────────┘ └──────────────┘

Сколько потоков в типичном Spring приложении

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class);
        
        // Сколько потоков работает?
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        int count = group.activeCount();
        System.out.println("Active threads: " + count);
    }
}

// Типичный вывод для Spring Boot:
// Active threads: 15-20

// Распределение потоков:
// - Main thread: 1
// - Tomcat (webserver) threads: 10-20
// - Spring background tasks: 1-3
// - Garbage Collector: 1-2
// - Other JVM threads: 2-3
// ИТОГО: ~15-30 потоков по умолчанию

Потребление памяти: Stack size

// Каждый thread выделяет память для стека
// По умолчанию:
// - 64-bit JVM: 1 MB на поток
// - 32-bit JVM: 320 KB на поток

// Вычисляем максимум потоков:
// Max threads = (Available Memory) / (Stack Size)

Example:
Server с 4 GB оперативной памяти:
4 GB / 1 MB = 4,000 потоков максимум

НО! Это теоретический максимум
На практике: ~500-1000 потоков, потом performance падает

Высокие нагрузки: Thread Pool

// ❌ НЕПРАВИЛЬНО: создаём поток на каждый запрос
@RestController
public class RequestHandler {
    @PostMapping("/process")
    public void handleRequest(Request request) {
        new Thread(() -> {
            process(request);  // ← Создаём новый thread!
        }).start();
        
        return ResponseEntity.ok();
    }
}

// 1000 запросов = 1000 новых потоков
// Это убьёт приложение (memory + context switching)

// ✅ ПРАВИЛЬНО: Thread Pool (reuse потоков)
@Configuration
public class ExecutorConfig {
    
    @Bean
    public Executor taskExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            10,           // Core threads (всегда готовые)
            50,           // Max threads
            60, TimeUnit.SECONDS,  // Keep-alive time
            new LinkedBlockingQueue<>(1000)  // Queue
        );
        return executor;
    }
}

@RestController
public class RequestHandler {
    
    @Autowired
    private Executor taskExecutor;
    
    @PostMapping("/process")
    public void handleRequest(Request request) {
        // Отправляем в pool (переиспользует thread из pool)
        taskExecutor.execute(() -> {
            process(request);
        });
        
        return ResponseEntity.ok();
    }
}

// 1000 запросов = 50 потоков (переиспользуемых из pool)

Диаграмма: Thread Pool работа

ThreadPoolExecutor:

Core Threads (10 потоков, всегда работают):
[T1] [T2] [T3] [T4] [T5] [T6] [T7] [T8] [T9] [T10]

Queue (待очередь задач):
[Task#1] [Task#2] [Task#3] ... [Task#100]

Max Threads (до 50 потоков):
Когда queue переполнена, создаются дополнительные потоки
[T11] [T12] ... [T50] (макс 40 дополнительных)

Процесс:
1. Task приходит → кладётся в queue
2. Core thread берёт task из queue
3. Executes task
4. Берёт следующий task
5. При нагрузке пике создаются дополнительные threads

Reentrant Lock и стеки

public class StackVsHeap {
    
    // На STACK (thread local):
    static void methodA() {
        int x = 10;  // ← На stack'е потока
        methodB();   // Stack frame добавляется
    }
    
    static void methodB() {
        int y = 20;  // ← На stack'е потока
        methodC();   // Stack frame добавляется
    }
    
    static void methodC() {
        int z = 30;  // ← На stack'е потока
        // Стек растёт:
        // methodA frame
        //   methodB frame
        //     methodC frame
    }
    
    // На HEAP (shared):
    static Object sharedObject = new Object();  // В heap'е
    
    static void methodWithHeap() {
        // Все потоки видят один и тот же sharedObject
        synchronized (sharedObject) {
            // Потокобезопасное изменение
        }
    }
}

Проблема: Stack Overflow

// ❌ Бесконечная рекурсия
static int recursion(int n) {
    return recursion(n + 1);  // Бесконечно добавляет frames в stack
}

public static void main(String[] args) {
    recursion(0);
    // StackOverflowError: каждый вызов добавляет frame
    // Когда stack переполнится (обычно ~10000 frames) → Error
}

// ✅ Правильно: базовый случай
static int factorial(int n) {
    if (n <= 1) return 1;  // ← Base case
    return n * factorial(n - 1);
}

Virtual Threads (Java 21+): Революция

// Java 21+: Virtual Threads (очень дешёвые потоки)

// ❌ Раньше: 1000 потоков = 1000 стеков = 1000 MB
ExecutorService executor = Executors.newFixedThreadPool(1000);

// ✅ Теперь: 100,000 virtual threads = почти бесплатно!
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 100_000; i++) {
    executor.submit(() -> {
        blockingIO();  // I/O без боли
    });
}

// Virtual threads:
// - Каждый имеет свой stack (но очень маленький)
// - Миллионы могут работать одновременно
// - Автоматически паркуются при I/O
// - Не требуют callback hell (async/await)

Мониторинг: сколько потоков работает

// Способ 1: Через JMX
java.lang.management.ThreadMXBean bean = 
    ManagementFactory.getThreadMXBean();

int threadCount = bean.getThreadCount();
int peakCount = bean.getPeakThreadCount();

System.out.println("Current threads: " + threadCount);
System.out.println("Peak threads: " + peakCount);

// Способ 2: Thread dump (jps, jstack)
// $ jps  # Найти PID
// $ jstack <PID>  # Выгрузить все потоки и их стеки

// Способ 3: Spring Boot Actuator
// GET http://localhost:8080/actuator/metrics/jvm.threads.live

Best Practices: Stack Efficiency

// ✅ 1. Используй Thread Pools, не создавай потоки вручную
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> work());

// ✅ 2. Контролируй глубину рекурсии
int MAX_RECURSION_DEPTH = 100;
int depth = 0;
void recursive() {
    if (depth > MAX_RECURSION_DEPTH) return;
    depth++;
    // работа
    recursive();
}

// ✅ 3. Локальные переменные лучше глобальных (на stack)
void method() {
    int localVar = 5;  // На stack (быстро)
    // vs
    globalVar = 5;     // В heap или static (медленнее)
}

// ✅ 4. Используй Virtual Threads в Java 21+
var executor = Executors.newVirtualThreadPerTaskExecutor();

Вывод

  • Стеков столько же, сколько потоков
  • В типичном Spring приложении: 15-30 потоков (15-30 стеков)
  • Каждый стек занимает: ~1 MB на 64-bit JVM
  • Максимум потоков: ограничен памятью (~4000 при 4GB RAM)
  • На практике: 50-500 потоков для веб-приложений
  • Никогда не создавай поток на каждый запрос
  • Используй Thread Pools для переиспользования
  • Java 21+: Virtual Threads предлагают миллионы "потоков" без штрафа