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

Какая связь между потоком оси и легковесным потоком?

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

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

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

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

Какая связь между потоком ОС и легковесным потоком?

Определения

Поток ОС (OS Thread / Native Thread)

  • Создаётся операционной системой
  • Имеет собственный стек вызовов
  • Требует переключение контекста ОС
  • Тяжелый (дорогой) в создании и управлении
  • Может выполняться параллельно на разных ядрах процессора

Легковесный поток (Virtual Thread / User-Level Thread)

  • Создаётся самой JVM или приложением
  • Множество легковесных потоков работают на одном потоке ОС
  • Нет переключения контекста ОС
  • Дешевый в создании и управлении
  • Кооперативная многозадачность (выход по явному условию)

Связь между ними

Поток ОС (тяжелый)
│
├─ Легковесный поток 1
├─ Легковесный поток 2
├─ Легковесный поток 3
└─ Легковесный поток 4

Один поток ОС может управлять несколькими легковесными потоками

Java Traditional Threads (Java 1-20)

// ❌ Старый подход — каждый Thread = один поток ОС
public void traditionalThreads() {
    // 1000 потоков = 1000 потоков ОС (дорого!)
    for (int i = 0; i < 1000; i++) {
        new Thread(() -> {
            blockingOperation();  // Блокирующая операция
        }).start();
    }
}

// Проблемы:
// 1. Каждый Thread требует ~1MB памяти
// 2. 1000 потоков = 1GB памяти только на стеки
// 3. Переключение контекста ОС очень дорого
// 4. Максимум ~10,000 потоков на типичной машине

Java Virtual Threads (Java 19+, Preview в 21, Released в 23)

// ✅ Новый подход — Virtual Threads
public void virtualThreads() {
    // 1,000,000 виртуальных потоков на фоне нескольких потоков ОС
    ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
    
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            blockingOperation();  // Легальная блокировка
        });
    }
}

// Преимущества:
// 1. Каждый Virtual Thread требует ~100 байт памяти
// 2. 1,000,000 потоков = ~100MB памяти
// 3. Нет переключения контекста ОС
// 4. Можно создать миллионы потоков

Архитектура Virtual Threads в Java

┌─────────────────────────────────────────────┐
│           Java Application                  │
├─────────────────────────────────────────────┤
│  Virtual Thread 1                           │
│  Virtual Thread 2                           │
│  Virtual Thread 3                           │
│  ...                                        │
│  Virtual Thread 1,000,000                   │
├─────────────────────────────────────────────┤
│        ForkJoinPool (Scheduler)             │
│      (например, 16 потоков)                 │
├─────────────────────────────────────────────┤
│        OS Thread 1                          │
│        OS Thread 2                          │
│        ...                                  │
│        OS Thread 16                         │
├─────────────────────────────────────────────┤
│     Operating System Kernel                 │
└─────────────────────────────────────────────┘

Как работают Virtual Threads

Концепция: Continuation

// Virtual Thread может быть "паркован" (suspended) при:
// - Блокирующем I/O (читка с диска, сети)
// - Ожидании монитора (synchronized)
// - Ожидании блокирующей очереди

public class VirtualThreadExample implements Runnable {
    @Override
    public void run() {
        System.out.println("1. Начало работы");
        
        blockingNetworkCall();  // Virtual Thread паркуется
        // Поток ОС освобождается, может обслуживать другой VT
        
        System.out.println("2. После блокирования");
        System.out.println("3. Конец работы");
    }
    
    private void blockingNetworkCall() {
        // Это прозрачное блокирование из точки зрения VT
        // Но для потока ОС это парковка, а не настоящая блокировка
    }
}

Создание Virtual Threads

Вариант 1: Явное создание

// Java 19+ (preview)
Thread vt = Thread.ofVirtual()
    .name("virtual-thread-1")
    .start(() -> {
        System.out.println("Virtual thread running");
    });

Вариант 2: ExecutorService

// Java 21+
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 1_000_000; i++) {
    final int taskId = i;
    executor.submit(() -> {
        System.out.println("Task " + taskId + " running on " + 
                          Thread.currentThread());
        // Результат: "Task 123 running on VirtualThread[#456]/scheduler-1"
    });
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);

Вариант 3: Structured Concurrency (Java 23+)

// Гарантирует завершение всех потоков
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var subtask1 = scope.fork(() -> compute(1));
    var subtask2 = scope.fork(() -> compute(2));
    var subtask3 = scope.fork(() -> compute(3));
    
    scope.join();  // Ждёт завершения всех
    
    System.out.println("Result: " + 
                      subtask1.resultNow() + 
                      subtask2.resultNow() + 
                      subtask3.resultNow());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Сравнение производительности

Traditional Threads vs Virtual Threads

public class PerformanceComparison {
    
    public static void main(String[] args) throws Exception {
        testTraditionalThreads();
        testVirtualThreads();
    }
    
    // ❌ Традиционные потоки
    static void testTraditionalThreads() {
        long startTime = System.currentTimeMillis();
        int count = 10_000;
        CountDownLatch latch = new CountDownLatch(count);
        
        for (int i = 0; i < count; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep(1000);  // Имитация работы
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Traditional: " + duration + "ms");
        // Вывод: ~12000ms (создание потоков + sleep)
    }
    
    // ✅ Virtual Threads
    static void testVirtualThreads() {
        long startTime = System.currentTimeMillis();
        int count = 10_000;
        
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < count; i++) {
                executor.submit(() -> {
                    try {
                        Thread.sleep(1000);  // Прозрачная парковка
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                });
            }
        }
        
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Virtual: " + duration + "ms");
        // Вывод: ~1100ms (только sleep, без overhead потоков)
    }
}

Когда Virtual Thread паркуется

// 1. Блокирующие I/O операции
SocketInputStream is = socket.getInputStream();
int data = is.read();  // Virtual Thread паркуется здесь

// 2. Ожидание на synchronize
synchronized(lock) {
    // Если lock занят, VT паркуется
}

// 3. Ожидание на monitor
wait();  // Virtual Thread паркуется

// 4. Thread.sleep()
Thread.sleep(1000);  // Virtual Thread паркуется, поток ОС свободен

// 5. Lock.lock() из java.util.concurrent
lock.lock();  // Если lock занят, VT паркуется

Ограничения Virtual Threads

// ❌ Что НЕ работает с Virtual Threads

// 1. Pinned потоки (некоторые операции)
synchronized(lock) {  // В Java 19-20 это пинит VT
    nativeCall();     // Native code pinит VT
}
// Решение в Java 21+: ReentrantLock вместо synchronized

// 2. ThreadLocal теряют смысл (много потоков)
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("value");
// На 1 млн потоков может быть проблема с памятью

// 3. Очень интенсивные вычисления без блокировок
for (int i = 0; i < 1_000_000_000; i++) {
    // Если нет блокировок, VT не может быть вытеснен
    // Используй Thread.yield()
}

Правильное использование

// ✅ Хороший кейс для Virtual Threads: I/O-bound операции
@RestController
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/users/{id}")
    public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
        // С Virtual Threads можно безопасно использовать
        // блокирующие операции в контроллере
        
        UserDTO user = userService.findById(id);  // Блокирует, но VT паркуется
        return ResponseEntity.ok(user);
    }
}

// ❌ Плохой кейс: CPU-bound операции
@Component
public class ComputeService {
    
    public int heavyComputation(int value) {
        // Для этого не нужны Virtual Threads
        // Используй ForkJoinPool для параллельных вычислений
        
        int result = value;
        for (int i = 0; i < 1_000_000_000; i++) {
            result *= 2;  // CPU bound, no I/O blocking
        }
        return result;
    }
}

Резюме

Традиционные Threads (Java < 19):

  • 1 Java Thread = 1 OS Thread
  • ~1MB памяти на thread
  • Максимум ~10K-50K threads
  • Тяжелое переключение контекста

Virtual Threads (Java 19+):

  • 1,000,000+ Java VT на ~16 OS Threads
  • ~100 байт памяти на VT
  • Легкое парковка/возобновление (continuation)
  • Идеальны для I/O-bound приложений (веб-серверы)

Ключевая идея: Virtual Threads позволяют писать простой, синхронный код (без async/await или reactive) при том, что под капотом используется асинхронная архитектура с минимальным overhead.