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

В чем разница между стеком и кучей в многопоточности?

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

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

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

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

Разница между стеком и кучей в многопоточности

В многопоточной среде стек и куча имеют совершенно разное поведение относительно потокобезопасности. Это одно из самых важных различий для понимания параллелизма в Java.

Stack (Стек)

Стек — это память, принадлежащая каждому потоку отдельно. Каждый поток имеет собственный стек.

public class StackExample {
    public void method() {
        int localVar = 42;    // Находится в STACK этого потока
        String name = "John"; // Ссылка в STACK, объект в HEAP
    }
}

// Поток 1: свой стек со своими localVar, name
// Поток 2: свой стек со своими localVar, name
// Поток 3: свой стек со своими localVar, name

Основные характеристики стека в многопоточности:

  • Каждому потоку свой стек — полная изоляция
  • Локальные переменные ВСЕГДА потокобезопасны — живут в собственном стеке
  • Нет race conditions для локальных переменных
  • Автоматическое удаление при выходе из метода
  • Меньший размер (обычно 1-8 MB на поток)

Heap (Куча)

Куча — это общая память, разделяемая всеми потоками приложения.

public class HeapExample {
    private int counter = 0;  // Находится в HEAP, разделяется всеми потоками!
    
    public void increment() {
        counter++;  // Race condition возможна!
    }
}

// Все потоки видят и модифицируют ОДИН counter объект в куче

Основные характеристики кучи в многопоточности:

  • Общая память для всех потоков — все видят одни и те же объекты
  • Race conditions возможны при одновременном доступе
  • Требуется синхронизация для безопасного доступа
  • Garbage collection управляет памятью
  • Большой размер (может быть несколько GB)

Практический пример: Stack vs Heap

public class MemoryThreading {
    // HEAP: все потоки видят один объект
    private List<String> sharedList = new ArrayList<>();
    
    // HEAP: все потоки видят один объект
    private int sharedCounter = 0;
    
    public void processInThread() {
        // STACK: каждый поток получает свой экземпляр
        int localCounter = 0;
        List<String> localList = new ArrayList<>();
        
        for (int i = 0; i < 1000; i++) {
            // STACK — безопасно
            localCounter++;  // Не race condition
            
            // HEAP — ОПАСНО!
            sharedCounter++;  // Race condition! Потеря обновлений
            
            // STACK ссылка, но объект в HEAP — ОПАСНО!
            sharedList.add("value");  // Могут быть concurrent modification
            
            // STACK — безопасно
            localList.add("value");  // Только этот поток видит
        }
    }
}

Визуализация памяти при 3 потоках

Поток 1 STACK          Поток 2 STACK          Поток 3 STACK
┌─────────────┐        ┌─────────────┐        ┌─────────────┐
│ i = 100     │        │ i = 200     │        │ i = 300     │
│ local = 50  │        │ local = 75  │        │ local = 25  │
└─────────────┘        └─────────────┘        └─────────────┘
       ↓                       ↓                       ↓
       └───────────────────────┴───────────────────────┘
                         HEAP (ОБЩАЯ)
               ┌──────────────────────────┐
               │ sharedCounter = 625      │ ← Race condition!
               │ sharedList = ["v", ...] │ ← Синхронизация нужна!
               └──────────────────────────┘

Когда Stack переменные потокобезопасны

public void readLocalVariables() {
    // ВСЕ ЭТИ ПЕРЕМЕННЫЕ В STACK — полностью потокобезопасны
    int count = 0;           // примитив в stack
    String name = "test";    // примитив (ссылка) в stack
    List<String> list = new ArrayList<>();  // ссылка в stack
    
    // Даже если вызвать из разных потоков одновременно,
    // каждый поток имеет свои count, name, list
}

// Пример: многопоточный код БЕЗ race condition
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        // Каждый поток: собственные переменные
        int threadLocal = i;      // STACK — безопасно
        String result = compute(); // STACK — безопасно
        System.out.println(result);  // Нет race condition
    });
}

Когда Heap объекты требуют синхронизации

public class UnsafeCounter {
    // HEAP: все потоки видят один объект
    private int count = 0;
    
    // ОПАСНО: race condition
    public void increment() {
        count++;  // Три операции: read, modify, write
    }
    
    // БЕЗОПАСНО: синхронизирован
    public synchronized void incrementSafe() {
        count++;  // Гарантировано атомарно
    }
}

// Проблема
UnsafeCounter counter = new UnsafeCounter();
IntStream.range(0, 10)
    .parallel()  // 10 потоков
    .forEach(i -> counter.increment());  // Race condition!

System.out.println(counter.count);  // Может быть не 10!

Thread Local Storage — промежуточное решение

// ThreadLocal — создает отдельные экземпляры для каждого потока
private static final ThreadLocal<SimpleDateFormat> dateFormat =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public String formatDate(Date date) {
    // Каждый поток получает свой SimpleDateFormat
    return dateFormat.get().format(date);  // Потокобезопасно
}

// Это как выделить каждому потоку свою область памяти
// вместо того, чтобы все делили один объект

Сравнение таблица

АспектSTACKHEAP
ВидимостьТолько для одного потокаВсем потокам
ПотокобезопасностьАвтоматическиТребуется синхронизация
Race conditionНевозможнаВозможна
Примерылокальные переменныеполя класса, объекты
Размер~1-8 MB на потокНесколько GB
ОчисткаАвтоматическаяGC
ПроизводительностьБыстрееМедленнее (синхронизация)

Практические правила

// ПРАВИЛО 1: Локальные переменные всегда потокобезопасны
public void threadSafeMethod() {
    int x = 10;  // Этот x только в моем потоке
    // Нет race condition, даже в многопоточной среде
}

// ПРАВИЛО 2: Поля класса требуют синхронизации
public class MyClass {
    private int counter = 0;  // ОПАСНО для многопоточности
    
    public synchronized void increment() {
        counter++;  // Теперь безопасно
    }
}

// ПРАВИЛО 3: Избегай объектов в HEAP если возможно
public void bad() {
    Integer shared = 0;  // HEAP!
    // Если передать в другие потоки — race condition
}

public void good() {
    int local = 0;  // STACK
    // Безопасно в многопоточности
}

Итог

Стек — локальная память каждого потока, полностью потокобезопасная. Куча — общая память всех потоков, требующая синхронизации при одновременном доступе. Понимание этого различия критично для написания корректного многопоточного кода. Используй локальные переменные (stack) везде, где возможно, и только когда необходимо делиться данными между потоками, размещай их в heap с соответствующей синхронизацией.

В чем разница между стеком и кучей в многопоточности? | PrepBro