Spring Boot 3.4 Virtual Threads: คำถามสัมภาษณ์และ Benchmark ประสิทธิภาพ

เชี่ยวชาญ Java 21 Virtual Threads ด้วย Spring Boot 3.4: 15 คำถามสัมภาษณ์ Benchmark ประสิทธิภาพ และรูปแบบการย้ายระบบสำหรับสอบสัมภาษณ์เทคนิค

Spring Boot 3.4 Virtual Threads: คำถามสัมภาษณ์และ Benchmark ประสิทธิภาพ

Virtual Threads คือหนึ่งในความก้าวหน้าที่สำคัญที่สุดของ Java 21 และ Spring Boot 3.4 รองรับอย่างเป็น native คุณสมบัติของ Project Loom นี้เปลี่ยนวิธีจัดการ concurrency ในแอปพลิเคชัน backend อย่างสิ้นเชิง การสัมภาษณ์เทคนิคในปัจจุบันจะวัดความเข้าใจกลไกภายใน กรณีใช้งานที่เหมาะสม และข้อผิดพลาดที่ควรหลีกเลี่ยง

คำแนะนำในการเตรียมตัว

ผู้สัมภาษณ์จะแยกผู้สมัครที่เข้าใจ Virtual Threads อย่างแท้จริงออกจากผู้ที่ใช้งานแบบไม่เข้าใจ การรู้ว่าเมื่อใดไม่ควรใช้นั้นสำคัญเทียบเท่ากับการรู้ประโยชน์ของมัน

พื้นฐาน Virtual Threads

คำถามที่ 1: Virtual Thread คืออะไรและต่างจาก Platform Thread อย่างไร

Virtual Thread คือ thread แบบเบาที่ JVM เป็นผู้บริหารจัดการ ไม่ใช่ระบบปฏิบัติการ ต่างจาก Platform Threads (thread แบบดั้งเดิม) ตรงที่ Virtual Threads ไม่ได้แมปตรงกับ thread ของ OS โดย JVM สามารถสร้างมันได้นับล้านโดยใช้หน่วยความจำเพียงเล็กน้อย

VirtualThreadDemo.javajava
// Comparing thread creation approaches
public class VirtualThreadDemo {

    public void demonstrateDifference() {
        // Platform Thread: ~1MB stack per thread
        // Practical limit: a few thousand on a standard JVM
        Thread platformThread = new Thread(() -> {
            performBlockingOperation();
        });

        // Virtual Thread: ~a few KB per thread
        // Can create millions without issues
        Thread virtualThread = Thread.ofVirtual().start(() -> {
            performBlockingOperation();
        });
    }

    // A Virtual Thread "mounts" onto a Platform Thread (carrier)
    // During I/O blocking, it releases the carrier for other Virtual Threads
    private void performBlockingOperation() {
        try {
            Thread.sleep(1000); // Virtual Thread detaches from carrier here
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

ความแตกต่างที่สำคัญที่สุดอยู่ที่พฤติกรรมเมื่อเกิดการ blocking เมื่อ Platform Thread ถูก block (I/O, sleep) มันจะยังผูกอยู่กับ thread ของ OS ขณะที่ Virtual Thread จะถอดตัวเอง (unmount) ออกจาก carrier thread เพื่อปล่อยให้ Virtual Threads ตัวอื่นใช้ carrier นั้นได้

คำถามที่ 2: เปิดใช้งาน Virtual Threads ใน Spring Boot 3.4 อย่างไร

Spring Boot 3.4 ลดการเปิดใช้งาน Virtual Threads ให้เหลือเพียง property การตั้งค่าเดียว เฟรมเวิร์กทั้งหมดจะปรับตัวอัตโนมัติ ทั้ง Tomcat, REST controller และการเรียกแบบ blocking ได้รับประโยชน์จากการปรับแต่งนี้ทันที

yaml
# application.yml
# Global Virtual Threads activation
spring:
  threads:
    virtual:
      enabled: true

# Optional Tomcat pool configuration
server:
  tomcat:
    threads:
      max: 200  # Less critical with Virtual Threads
      min-spare: 10
WebConfig.javajava
// Programmatic activation if needed
@Configuration
public class WebConfig {

    @Bean
    public TomcatProtocolHandlerCustomizer<?> virtualThreadCustomizer() {
        // Each HTTP request runs on a Virtual Thread
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

การเปิดใช้งานเปลี่ยนพฤติกรรมของ Tomcat แทนที่จะใช้ thread pool ขนาดคงที่ ทุก request จะได้รับ Virtual Thread เป็นของตัวเอง วิธีการนี้ขจัดคอขวดของ thread pool แบบเดิม

คำถามที่ 3: อธิบายแนวคิด "mounting" และ "unmounting"

Mounting หมายถึงการผูก Virtual Thread เข้ากับ carrier thread (Platform Thread) ส่วน unmounting เกิดขึ้นระหว่างการดำเนินการแบบ blocking โดยจะปล่อย carrier ให้ Virtual Threads อื่นได้ใช้ กลไกนี้ทำให้ใช้ทรัพยากร CPU ได้อย่างมีประสิทธิภาพสูงสุด

MountingDemo.javajava
// Illustrating the mounting/unmounting cycle
public class MountingDemo {

    public void demonstrateMounting() {
        Thread.ofVirtual().start(() -> {
            // MOUNTED: Virtual Thread is using a carrier thread
            System.out.println("Carrier: " + getCurrentCarrier());

            // UNMOUNTING: releases carrier during blocking
            performDatabaseQuery(); // Blocking JDBC call

            // REMOUNTED: may be on a different carrier
            System.out.println("New carrier: " + getCurrentCarrier());
        });
    }

    // Blocking operations trigger unmounting automatically
    private void performDatabaseQuery() {
        // JDBC connection, file read, network call...
        // Virtual Thread detaches during I/O wait
    }

    private String getCurrentCarrier() {
        // Gets the current carrier thread name
        return Thread.currentThread().toString();
    }
}

กลไกนี้โปร่งใสสำหรับนักพัฒนา โค้ดยังเขียนในรูปแบบ imperative คลาสสิก แต่ JVM จะปรับการใช้ carrier ให้อัตโนมัติ pool ที่มี carrier เพียงไม่กี่ตัวก็สามารถรองรับ Virtual Thread นับล้านได้

หมายเหตุทางเทคนิค

จำนวน carrier thread โดยปริยายจะเท่ากับจำนวน core ของ CPU JVM ปรับขนาด pool นี้แบบไดนามิกผ่าน ForkJoinPool

กรณีใช้งานและ Anti-pattern

คำถามที่ 4: Virtual Threads ให้ผลด้านประสิทธิภาพเมื่อใด

Virtual Threads เปล่งประกายกับงาน I/O-bound เช่น การเรียก REST ภายนอก การ query ฐานข้อมูล หรือการอ่านไฟล์ ปฏิบัติการเหล่านี้ใช้เวลาส่วนใหญ่รอ ในช่วงเวลาดังกล่าว Virtual Thread จะปล่อย carrier ของตัวเอง

IOBoundService.javajava
// Ideal case for Virtual Threads
@Service
public class IOBoundService {

    private final RestClient restClient;
    private final UserRepository userRepository;

    // Each call involves network and database waiting
    // Virtual Threads shine here
    public UserProfile enrichUserProfile(Long userId) {
        // DB call - VT detaches during SQL query
        User user = userRepository.findById(userId).orElseThrow();

        // External REST call - VT detaches during HTTP wait
        ExternalData externalData = restClient
            .get()
            .uri("/api/external/{id}", userId)
            .retrieve()
            .body(ExternalData.class);

        // Data aggregation
        return new UserProfile(user, externalData);
    }
}

ผลตอบแทนมาจากการรองรับ request พร้อมกันได้มากขึ้น Platform Threads จำนวน 200 และ request ละ 100ms ให้ throughput สูงสุดเพียง 2,000 req/s ในขณะที่ Virtual Threads สามารถไปถึงกว่า 50,000 req/s บนเครื่องเดียวกัน

คำถามที่ 5: ควรหลีกเลี่ยง Anti-pattern ใดเมื่อใช้ Virtual Threads

Virtual Threads ไม่เหมาะกับงาน CPU-bound หรือสถานการณ์ที่ทำให้เกิด "pinning" pinning เกิดขึ้นเมื่อ Virtual Thread ยังคงผูกกับ carrier แม้จะ block อยู่ ทำให้ประโยชน์ของการทำ virtualization หายไป

AntiPatterns.javajava
// Examples of cases to avoid
@Service
public class AntiPatterns {

    // ANTI-PATTERN 1: CPU-intensive computation
    // Virtual Threads provide no benefit here
    public BigInteger computeFactorial(int n) {
        // 100% CPU, no I/O, no unmounting possible
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
        }
        return result; // Carrier is monopolized throughout
    }

    // ANTI-PATTERN 2: Synchronized causes pinning
    private final Object lock = new Object();

    public void pinnedOperation() {
        synchronized (lock) { // PINNING: VT stays on carrier
            performDatabaseQuery(); // Unmounting doesn't happen!
        }
    }

    // SOLUTION: Use ReentrantLock
    private final ReentrantLock reentrantLock = new ReentrantLock();

    public void unpinnedOperation() {
        reentrantLock.lock();
        try {
            performDatabaseQuery(); // Unmounting possible
        } finally {
            reentrantLock.unlock();
        }
    }
}

ในแง่ทรัพยากร pinning เปลี่ยน Virtual Thread ให้กลายเป็น Platform Thread สาเหตุหลักคือบล็อก synchronized และการเรียก JNI แบบ native การเปลี่ยนไปใช้ ReentrantLock แก้ปัญหาแรกได้

คำถามที่ 6: ตรวจจับ pinning ในแอปพลิเคชันได้อย่างไร

JVM มีตัวเลือกการวินิจฉัยเพื่อระบุกรณี pinning ข้อมูลนี้สำคัญมากในการย้ายระบบไป Virtual Threads เพราะ pinning อาจทำให้ประสิทธิภาพแย่ลงแทนที่จะดีขึ้น

PinningDiagnostics.javajava
// Configuration and pinning detection
public class PinningDiagnostics {

    // JVM option to log pinning
    // -Djdk.tracePinnedThreads=full (detailed)
    // -Djdk.tracePinnedThreads=short (summary)

    // Example code causing pinning
    public void demonstratePinning() {
        Thread.ofVirtual().start(() -> {
            synchronized (this) {
                // This log will appear with tracePinnedThreads enabled
                try {
                    Thread.sleep(100); // Pinned during sleep!
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
}
bash
# Typical output with -Djdk.tracePinnedThreads=full
Thread[#23,VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
    java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:661)
    com.example.PinningDiagnostics.demonstratePinning(PinningDiagnostics.java:15)

การวิเคราะห์ log ของ pinning จะบ่งชี้จุดร้อนที่ต้องแก้ไข แอปพลิเคชันที่เกิด pinning บ่อยจะใช้ประโยชน์จาก Virtual Threads ได้ไม่เต็มที่ และอาจทำงานช้ากว่าการใช้ Platform Threads ด้วยซ้ำ

พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Benchmark ประสิทธิภาพ

คำถามที่ 7: คาดหวังการเพิ่มประสิทธิภาพได้แค่ไหนจาก Virtual Threads

Benchmark แสดงการปรับปรุงอย่างมีนัยสำคัญในแอปพลิเคชัน I/O-bound ทั่วไป ผลตอบแทนขึ้นกับสัดส่วนระหว่างเวลา I/O กับเวลา CPU และระดับ concurrency ที่ต้องการ

BenchmarkController.javajava
// Endpoint for measuring performance
@RestController
@RequestMapping("/api/benchmark")
public class BenchmarkController {

    private final ExternalApiClient apiClient;

    // Simulation of a typical endpoint with external calls
    @GetMapping("/user/{id}")
    public ResponseEntity<UserData> getUser(@PathVariable Long id) {
        // Three sequential I/O calls
        UserInfo info = apiClient.fetchUserInfo(id);        // ~50ms
        List<Order> orders = apiClient.fetchOrders(id);     // ~80ms
        CreditScore score = apiClient.fetchCreditScore(id); // ~100ms

        return ResponseEntity.ok(new UserData(info, orders, score));
    }
}
text
# Benchmark results - 10,000 concurrent requests
# Configuration: 8 cores, 16GB RAM, simulated latency 230ms/request

Platform Threads (pool 200):
- Throughput: 850 req/s
- P99 latency: 1250ms
- Heap memory: 2.1GB

Virtual Threads:
- Throughput: 4200 req/s
- P99 latency: 280ms
- Heap memory: 850MB

Gain: 5x throughput, 4.5x P99 latency reduction

Virtual Threads ยังลดการใช้หน่วยความจำลง เนื่องจากแต่ละ thread จองหน่วยความจำเพียงไม่กี่ KB แทนที่จะเป็นประมาณ 1 MB ของ Platform Thread

คำถามที่ 8: ควรตั้งค่า connection pool อย่างไรเมื่อใช้ Virtual Threads

Connection pool (HikariCP, Lettuce) จะกลายเป็นคอขวดใหม่เมื่อใช้ Virtual Threads pool ที่มี 10 connection จะจำกัด query ฐานข้อมูลพร้อมกันไว้ที่ 10 ครั้ง แม้จะมี Virtual Thread นับล้านก็ตาม

yaml
# application.yml
# HikariCP configuration optimized for Virtual Threads
spring:
  datasource:
    hikari:
      # More connections since Virtual Threads allow more concurrency
      maximum-pool-size: 50
      minimum-idle: 10
      # Shorter timeout due to more concurrent requests
      connection-timeout: 5000
      # Fast validation
      validation-timeout: 3000

  # Redis with Lettuce - already async-friendly
  data:
    redis:
      lettuce:
        pool:
          max-active: 50
          max-idle: 20
ConnectionPoolMonitor.javajava
// Pool monitoring to avoid contention
@Component
public class ConnectionPoolMonitor {

    private final HikariDataSource dataSource;

    @Scheduled(fixedRate = 10000)
    public void logPoolStats() {
        HikariPoolMXBean pool = dataSource.getHikariPoolMXBean();
        log.info("Pool stats - Active: {}, Idle: {}, Waiting: {}",
            pool.getActiveConnections(),
            pool.getIdleConnections(),
            pool.getThreadsAwaitingConnection());

        // Alert if too many threads waiting
        if (pool.getThreadsAwaitingConnection() > 100) {
            log.warn("Connection pool contention detected!");
        }
    }
}

ขนาดของ pool ขึ้นอยู่กับความสามารถของฐานข้อมูล ไม่ใช่จำนวน Virtual Threads PostgreSQL มาตรฐานรองรับการเชื่อมต่อใช้งานพร้อมกันราว 100-200 connection

คำถามที่ 9: วัดผลกระทบของ Virtual Threads ด้วย Micrometer ได้อย่างไร

Micrometer และ Spring Boot Actuator ให้ตัวชี้วัดสำคัญสำหรับประเมินประสิทธิภาพของ Virtual Threads ตัวชี้วัดเหล่านี้ยืนยันผลตอบแทนและเปิดเผยปัญหาที่อาจซ่อนอยู่

VirtualThreadMetrics.javajava
// Custom metrics for Virtual Threads
@Component
public class VirtualThreadMetrics {

    private final MeterRegistry registry;
    private final AtomicLong activeVirtualThreads = new AtomicLong(0);

    @PostConstruct
    public void registerMetrics() {
        // Active Virtual Threads counter
        Gauge.builder("virtual.threads.active", activeVirtualThreads, AtomicLong::get)
            .description("Number of active virtual threads")
            .register(registry);

        // JVM metrics for carriers
        Gauge.builder("virtual.threads.carriers", this::getCarrierCount)
            .description("Number of carrier threads")
            .register(registry);
    }

    private double getCarrierCount() {
        // Gets the ForkJoinPool carrier count
        return ForkJoinPool.commonPool().getPoolSize();
    }

    // Interceptor to trace requests
    public void trackVirtualThread(Runnable task) {
        activeVirtualThreads.incrementAndGet();
        try {
            task.run();
        } finally {
            activeVirtualThreads.decrementAndGet();
        }
    }
}
yaml
# application.yml
# Metrics exposure
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

การวิเคราะห์ตัวชี้วัดเผยให้เห็นสัดส่วนระหว่าง Virtual Threads และ Carriers พร้อมระบุช่วงเวลาที่เกิด contention บน connection pool

การย้ายระบบและความเข้ากันได้

คำถามที่ 10: ไลบรารีใดที่เข้ากันได้กับ Virtual Threads

ความเข้ากันได้ขึ้นอยู่กับการใช้บล็อก synchronized และการเรียกแบบ native ระบบนิเวศของ Spring เข้ากันได้ในวงกว้าง แต่ไลบรารีบางตัวยังต้องการเวอร์ชันเฉพาะ

CompatibilityCheck.javajava
// Checking dependency compatibility
@Configuration
public class CompatibilityCheck {

    // Compatible libraries (recommended versions)
    // - Spring Boot 3.2+ (native support)
    // - HikariCP 5.1+ (ReentrantLock instead of synchronized)
    // - Lettuce 6.3+ (non-blocking I/O)
    // - Jackson 2.16+ (no synchronized)

    // Libraries requiring attention
    // - JDBC drivers: check version
    // - Some legacy HTTP clients

    @Bean
    public CommandLineRunner checkCompatibility() {
        return args -> {
            // Log versions for audit
            log.info("Java version: {}", System.getProperty("java.version"));
            log.info("Virtual threads available: {}",
                Thread.ofVirtual() != null);

            // Support verification
            if (Runtime.version().feature() < 21) {
                throw new IllegalStateException(
                    "Java 21+ required for Virtual Threads");
            }
        };
    }
}

เฟรมเวิร์กสมัยใหม่ส่วนใหญ่ย้ายมาใช้ ReentrantLock แล้ว สำหรับ dependency รุ่นเก่า การทดสอบโหลดด้วย -Djdk.tracePinnedThreads=short จะช่วยเปิดเผยปัญหา pinning

คำถามที่ 11: จะย้ายระบบไป Virtual Threads อย่างค่อยเป็นค่อยไปได้อย่างไร

การย้ายแบบค่อยเป็นค่อยไปทำให้สามารถยืนยันผลตอบแทนและตรวจพบปัญหาได้โดยไม่กระทบต่อ production กลยุทธ์ที่แนะนำคือแยก endpoint ที่ใช้ Virtual Threads ออกจาก endpoint แบบเดิม

DualExecutorConfig.javajava
// Configuration for progressive migration
@Configuration
public class DualExecutorConfig {

    // Traditional executor for legacy endpoints
    @Bean("platformExecutor")
    public ExecutorService platformExecutor() {
        return Executors.newFixedThreadPool(200);
    }

    // Virtual Threads executor for new endpoints
    @Bean("virtualExecutor")
    public ExecutorService virtualExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

@RestController
@RequestMapping("/api/v2")
public class MigratedController {

    @Qualifier("virtualExecutor")
    private final ExecutorService executor;

    // Endpoint migrated to Virtual Threads
    @GetMapping("/users/{id}")
    public CompletableFuture<User> getUser(@PathVariable Long id) {
        return CompletableFuture.supplyAsync(() -> {
            // Business code unchanged
            return userService.findById(id);
        }, executor);
    }
}
yaml
# application.yml
# Feature flag for migration
features:
  virtual-threads:
    enabled: true
    endpoints:
      - /api/v2/**
      - /api/reports/**

# Disable for quick rollback
# features.virtual-threads.enabled: false
ข้อควรระวัง

ไม่ควรเปิด Virtual Threads แบบ global ก่อนที่จะทดสอบ dependency ทั้งหมด pinning อาจทำให้ประสิทธิภาพแย่ลงอย่างมีนัยสำคัญ

คำถามที่ 12: ต้องทดสอบอะไรบ้างก่อนนำขึ้น production

การตรวจสอบ Virtual Threads ต้องอาศัยการทดสอบโหลด การทดสอบ pinning และการทดสอบความเข้ากันได้ การทดสอบเหล่านี้ต้องจำลองสภาพ production ตามจริง

VirtualThreadLoadTest.javajava
// Load test with JUnit and Virtual Threads
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class VirtualThreadLoadTest {

    @LocalServerPort
    private int port;

    @Test
    void shouldHandleHighConcurrency() throws Exception {
        int concurrentRequests = 5000;
        CountDownLatch latch = new CountDownLatch(concurrentRequests);
        AtomicInteger successCount = new AtomicInteger(0);
        AtomicInteger errorCount = new AtomicInteger(0);

        HttpClient client = HttpClient.newHttpClient();

        // Launch 5000 simultaneous requests
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < concurrentRequests; i++) {
                executor.submit(() -> {
                    try {
                        HttpRequest request = HttpRequest.newBuilder()
                            .uri(URI.create("http://localhost:" + port + "/api/test"))
                            .build();

                        HttpResponse<String> response = client.send(
                            request, BodyHandlers.ofString());

                        if (response.statusCode() == 200) {
                            successCount.incrementAndGet();
                        } else {
                            errorCount.incrementAndGet();
                        }
                    } catch (Exception e) {
                        errorCount.incrementAndGet();
                    } finally {
                        latch.countDown();
                    }
                });
            }
        }

        latch.await(60, TimeUnit.SECONDS);

        // Performance assertions
        assertThat(successCount.get()).isGreaterThan(4900); // >98% success
        assertThat(errorCount.get()).isLessThan(100);
    }
}

การทดสอบควรครอบคลุมสถานการณ์ contention (connection pool ตัน) timeouts และโหลดต่อเนื่องเป็นเวลาหลายนาที

คำถามขั้นสูง

คำถามที่ 13: Virtual Threads ทำงานร่วมกับ Structured Concurrency อย่างไร

Structured Concurrency (JEP 453) เสริม Virtual Threads ด้วยการรับประกันว่า task ที่ทำงานพร้อมกันมี lifecycle ร่วมกัน วิธีการนี้ทำให้การจัดการข้อผิดพลาดและการยกเลิกงานง่ายขึ้น

StructuredConcurrencyExample.javajava
// Combining Virtual Threads + Structured Concurrency
public class StructuredConcurrencyExample {

    public UserDashboard fetchDashboard(Long userId) throws Exception {
        // StructuredTaskScope ensures all tasks complete together
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

            // Three parallel tasks on Virtual Threads
            Subtask<UserProfile> profileTask = scope.fork(() ->
                userService.getProfile(userId));

            Subtask<List<Notification>> notifTask = scope.fork(() ->
                notificationService.getRecent(userId));

            Subtask<AccountBalance> balanceTask = scope.fork(() ->
                accountService.getBalance(userId));

            // Wait for all tasks or fail on first error
            scope.join();
            scope.throwIfFailed();

            // All tasks succeeded - safe aggregation
            return new UserDashboard(
                profileTask.get(),
                notifTask.get(),
                balanceTask.get()
            );
        }
        // If one task fails, others are automatically cancelled
    }
}

Structured Concurrency ช่วยป้องกัน thread leak และทำให้การ debug ง่ายขึ้น เพราะ call stack สะท้อนโครงสร้างเชิงตรรกะของโค้ด

คำถามที่ 14: Virtual Threads ต่างจาก reactive programming อย่างไร

ทั้งสองแนวทางแก้ปัญหาเดียวกัน (ประสิทธิภาพ I/O) แต่ใช้รูปแบบการเขียนโปรแกรมต่างกัน Virtual Threads ให้เขียนโค้ดแบบ imperative คลาสสิก ในขณะที่ reactive ต้องเขียนใหม่ในรูปแบบ stream

ComparisonService.javajava
// Same logic: imperative vs reactive
@Service
public class ComparisonService {

    // VIRTUAL THREADS APPROACH: classic imperative code
    // Easy to read, debug, and maintain
    public UserData getUserDataImperative(Long id) {
        User user = userRepository.findById(id).orElseThrow();
        List<Order> orders = orderRepository.findByUserId(id);
        PaymentInfo payment = paymentService.getInfo(id);

        return new UserData(user, orders, payment);
    }

    // REACTIVE APPROACH: streams and operators
    // More complex but native backpressure
    public Mono<UserData> getUserDataReactive(Long id) {
        return userRepository.findById(id)
            .zipWith(orderRepository.findByUserId(id).collectList())
            .zipWith(paymentService.getInfo(id))
            .map(tuple -> new UserData(
                tuple.getT1().getT1(),
                tuple.getT1().getT2(),
                tuple.getT2()
            ));
    }
}

| เกณฑ์ | Virtual Threads | Reactive | |-------|-----------------|----------| | เส้นโค้งการเรียนรู้ | ต่ำ | สูง | | Debug | Stack trace แบบคลาสสิก | ซับซ้อน | | Backpressure | ทำเอง | Native | | ระบบนิเวศ | กำลังเติบโต | สมบูรณ์ | | ย้าย legacy | ง่าย | เขียนใหม่ |

Virtual Threads แนะนำสำหรับแอปใหม่และการย้ายระบบ ส่วน reactive ยังเหมาะกับงานที่ต้องการ backpressure ซับซ้อน

คำถามที่ 15: จัดการ ThreadLocal กับ Virtual Threads อย่างไร

ThreadLocal ใช้ได้กับ Virtual Threads แต่กินหน่วยความจำต่อ instance Scoped Values (JEP 446) เป็นทางเลือกที่มีประสิทธิภาพมากกว่าในการแชร์ context

ThreadLocalVsScopedValue.javajava
// Comparing context approaches
public class ThreadLocalVsScopedValue {

    // CLASSIC APPROACH: ThreadLocal
    // Works but expensive with millions of VT
    private static final ThreadLocal<RequestContext> requestContext =
        new ThreadLocal<>();

    public void processWithThreadLocal(Request request) {
        requestContext.set(new RequestContext(request.getTraceId()));
        try {
            // Context accessible everywhere in the thread
            processRequest();
        } finally {
            requestContext.remove(); // Important to prevent leaks
        }
    }

    // MODERN APPROACH: ScopedValue (Java 21+)
    // More efficient, immutable, explicit scope
    private static final ScopedValue<RequestContext> CONTEXT =
        ScopedValue.newInstance();

    public void processWithScopedValue(Request request) {
        ScopedValue.where(CONTEXT, new RequestContext(request.getTraceId()))
            .run(() -> {
                // Context accessible within this scope
                processRequest();
                // No cleanup needed - automatic at scope end
            });
    }

    private void processRequest() {
        // Context access
        String traceId = CONTEXT.isBound()
            ? CONTEXT.get().getTraceId()
            : "unknown";
        log.info("Processing with trace: {}", traceId);
    }
}

ScopedValues แนะนำสำหรับงานพัฒนาใหม่ ส่วนโค้ดเก่าที่ใช้ ThreadLocal ควรย้ายระบบทีละน้อย

พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

บทสรุป

Virtual Threads เปลี่ยนแปลงการพัฒนา backend ของ Java ด้วยการเขียนโค้ดแบบเรียบง่ายแต่ประสิทธิภาพสูง ประเด็นสำคัญ:

พื้นฐาน:

  • เปิดใช้งานด้วย spring.threads.virtual.enabled=true
  • เหมาะกับงาน I/O-bound (REST, DB, ไฟล์)
  • หลีกเลี่ยงงานที่ใช้ CPU หนัก

ประสิทธิภาพ:

  • กำหนดขนาด connection pool ให้เหมาะสม (คอขวดใหม่)
  • ติดตาม pinning ด้วย -Djdk.tracePinnedThreads
  • ย้าย synchronized ไปใช้ ReentrantLock

การย้ายระบบ:

  • ทดสอบเป็นกลุ่ม endpoint อย่างค่อยเป็นค่อยไป
  • ตรวจสอบความเข้ากันได้ของ dependency
  • ใช้ Structured Concurrency กับงานขนาน

ความเชี่ยวชาญใน Virtual Threads ทำให้ผู้สมัครที่เข้าใจความท้าทายด้านประสิทธิภาพของแอปสมัยใหม่โดดเด่นออกมา แนวคิดเหล่านี้กลายเป็นสิ่งจำเป็นในการสัมภาษณ์ Spring Boot ในปัจจุบันแล้ว

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#spring boot
#virtual threads
#java 21
#project loom
#performance

แชร์

บทความที่เกี่ยวข้อง