Back to blog

Go Goroutines vs Java Virtual Threads: So Sánh Toàn Diện

gojavaconcurrencygoroutinesvirtual-threadscomparison
Go Goroutines vs Java Virtual Threads: So Sánh Toàn Diện

Cả Go goroutines và Java Virtual Threads (Project Loom) đều cách mạng hóa cách chúng ta viết concurrent code. Nhưng nên chọn cái nào? Trong so sánh toàn diện này, chúng ta sẽ khám phá architecture, performance, syntax, và real-world use cases của chúng.

Giới Thiệu

Thách thức Concurrency: Traditional OS threads đắt (1-2 MB stack per thread), giới hạn scalability. Cả Go và Java đều giải quyết vấn đề này với lightweight concurrency primitives:

  • Go: Goroutines (từ Go 1.0, 2012)
  • Java: Virtual Threads (Java 21, September 2023)

Mặc dù giải quyết problems tương tự, approaches và philosophies của chúng khác nhau đáng kể.


Bảng So Sánh Nhanh

Tính năngGo GoroutinesJava Virtual Threads
Released2012 (Go 1.0)2023 (Java 21)
Memory per unit~2 KB initial~1 KB initial
Max concurrencyHàng triệuHàng triệu
SchedulingM:N (custom scheduler)M:N (JVM scheduler)
Syntaxgo func()Thread.startVirtualThread()
CommunicationChannels (CSP model)Shared memory
Learning curveThấpTrung bình
Ecosystem maturityMature (12+ năm)Mới (1+ năm)
Backward compatibilityN/A (ngôn ngữ mới)Full Java compatibility

Architecture: Cách Chúng Hoạt Động

Go Goroutines: M:N Scheduler với GMP Model

Components:

  • G (Goroutine): Lightweight execution unit (~2 KB)
  • M (Machine): OS thread
  • P (Processor): Scheduling context

Key Features:

  • Work stealing: Idle P steals goroutines từ busy P
  • Preemptive scheduling: Goroutines có thể bị preempted
  • Integrated với Go runtime: Garbage collector aware

Java Virtual Threads: JVM Carrier Threads

Components:

  • Virtual Thread: Lightweight thread (~1 KB)
  • Carrier Thread: Platform thread chạy virtual threads
  • ForkJoinPool: Default scheduler

Key Features:

  • Mounts/Unmounts: Virtual threads mount trên carrier threads
  • Blocking operations: Automatically unmount during I/O
  • Full Thread API: Dùng existing java.lang.Thread API

So Sánh Syntax

Tạo Lightweight Concurrency Units

Go:

package main
 
import (
    "fmt"
    "time"
)
 
func worker(id int) {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(1 * time.Second)
    fmt.Printf("Worker %d done\n", id)
}
 
func main() {
    for i := 1; i <= 3; i++ {
        go worker(i) // Launch goroutine
    }
 
    time.Sleep(2 * time.Second)
}

Java:

import java.util.concurrent.Executors;
 
public class Main {
    static void worker(int id) {
        System.out.printf("Worker %d starting\n", id);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.printf("Worker %d done\n", id);
    }
 
    public static void main(String[] args) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 3; i++) {
                final int id = i;
                executor.submit(() -> worker(id));
            }
        }
    }
}

Winner cho simplicity: Go (cleaner syntax với go keyword)


Communication Patterns

Go: Channels (CSP - Communicating Sequential Processes)

package main
 
import "fmt"
 
func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i // Send to channel
    }
    close(ch)
}
 
func consumer(ch <-chan int) {
    for value := range ch { // Receive from channel
        fmt.Println("Received:", value)
    }
}
 
func main() {
    ch := make(chan int)
 
    go producer(ch)
    consumer(ch)
}

Go Philosophy: "Don't communicate by sharing memory; share memory by communicating."

Java: Shared Memory (Traditional Approach)

import java.util.concurrent.*;
 
public class Main {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
 
        // Producer virtual thread
        Thread.startVirtualThread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    queue.put(i);
                }
                queue.put(-1); // Sentinel value
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
 
        // Consumer virtual thread
        Thread.startVirtualThread(() -> {
            try {
                while (true) {
                    int value = queue.take();
                    if (value == -1) break;
                    System.out.println("Received: " + value);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

So sánh:

  • Go: Built-in channels, type-safe, dễ reason about
  • Java: Dùng BlockingQueue, verbose hơn nhưng familiar với Java developers

Performance Comparison

Memory Usage

Go Goroutines:

1,000 goroutines    = ~2 MB
10,000 goroutines   = ~20 MB
100,000 goroutines  = ~200 MB
1,000,000 goroutines = ~2 GB

Java Virtual Threads:

1,000 virtual threads    = ~1 MB
10,000 virtual threads   = ~10 MB
100,000 virtual threads  = ~100 MB
1,000,000 virtual threads = ~1 GB

Winner: Java (slightly lower memory footprint)

Benchmark: Tạo 1 Million Concurrent Tasks

Go: Típical result ~1-3 giây

Java: Típical result ~2-4 giây

Winner: Tie (cả hai handle millions concurrent tasks efficiently)


Real-World Example: HTTP Server

Go: HTTP Server với Goroutines

package main
 
import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)
 
func fetchURL(url string, wg *sync.WaitGroup, results chan<- string) {
    defer wg.Done()
 
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        results <- fmt.Sprintf("Error: %v", err)
        return
    }
    defer resp.Body.Close()
 
    body, _ := io.ReadAll(resp.Body)
    results <- fmt.Sprintf("%s: %d bytes in %v",
        url, len(body), time.Since(start))
}
 
func main() {
    urls := []string{
        "https://golang.org",
        "https://github.com",
        "https://stackoverflow.com",
    }
 
    var wg sync.WaitGroup
    results := make(chan string, len(urls))
 
    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg, results)
    }
 
    go func() {
        wg.Wait()
        close(results)
    }()
 
    for result := range results {
        fmt.Println(result)
    }
}

So sánh:

  • Go: Concise hơn với channels, goroutines đơn giản hơn
  • Java: Verbose hơn nhưng dùng familiar HTTP client API

Use Cases: Khi Nào Dùng Mỗi Loại

Dùng Go Goroutines Khi:

✅ Building microservices - Simple, fast, dễ deploy
✅ Cloud-native applications - Docker, Kubernetes-friendly
✅ CLI tools - Single binary distribution
✅ Network programming - Low-level sockets, proxies
✅ Starting new project - Không có legacy code constraints
✅ Team thích simplicity - Easier learning curve

Example Use Cases:

  • Docker (container runtime)
  • Kubernetes (orchestration)
  • Terraform (infrastructure as code)
  • CockroachDB (distributed database)

Dùng Java Virtual Threads Khi:

✅ Existing Java codebase - Upgrade mà không cần rewrite
✅ Enterprise applications - Spring Boot, Jakarta EE
✅ Large teams - Mature tooling và ecosystem
✅ Complex business logic - Strong typing, OOP patterns
✅ Database-heavy apps - JDBC, Hibernate, JPA
✅ Regulatory compliance - Java's stability và support

Example Use Cases:

  • Banking systems
  • E-commerce platforms (Spring Boot)
  • Enterprise resource planning (ERP)
  • Legacy system modernization

Migration Path

Migrating Java Platform Threads sang Virtual Threads

Before (Platform Threads):

ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(() -> handleRequest());

After (Virtual Threads):

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> handleRequest());

Chỉ vậy thôi! Không cần code changes trong handleRequest().

Migrating sang Go Goroutines

Requires complete rewrite:

  • Different language syntax
  • Channel-based communication
  • Error handling patterns
  • Ecosystem differences

Không phải migration, mà là new implementation.


Limitations và Gotchas

Go Goroutines

Limitations:

  • ❌ Không có goroutine IDs (by design)
  • ❌ Không thể kill goroutine từ outside
  • ❌ Goroutine leaks nếu không manage properly
  • ❌ Channel deadlocks có thể tricky để debug

Gotchas:

// ❌ BAD: Capturing loop variable
for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(i) // Race condition!
    }()
}
 
// ✅ GOOD: Pass as argument
for i := 0; i < 10; i++ {
    go func(id int) {
        fmt.Println(id)
    }(i)
}

Java Virtual Threads

Limitations:

  • ❌ Pinning issues (synchronized blocks, native calls)
  • ❌ Thread-local variables có thể cause memory leaks
  • ❌ Không phải tất cả libraries đều virtual thread-aware
  • ❌ Stack traces có thể confusing

Gotchas:

// ❌ PINNING: synchronized blocks pin to carrier thread
synchronized (lock) {
    blockingOperation(); // Pins carrier thread!
}
 
// ✅ BETTER: Use ReentrantLock
lock.lock();
try {
    blockingOperation(); // Doesn't pin
} finally {
    lock.unlock();
}

Learning Curve

Go

Beginner-Friendly:

  • Simple go keyword
  • Built-in channels
  • Minimal boilerplate
  • Clear documentation

Time to Productivity: 1-2 tuần

Java Virtual Threads

Moderate Learning Curve:

  • Hiểu existing Thread API
  • Học new patterns (StructuredTaskScope)
  • Avoid pinning issues
  • Migration considerations

Time to Productivity: 2-4 tuần (nếu đã biết Java)


Tóm Tắt và Key Takeaways

✅ Go Goroutines: Simpler syntax, built-in channels, lightweight, tốt cho new projects
✅ Java Virtual Threads: Massive ecosystem, enterprise-ready, backward compatible, tốt cho existing Java codebases
✅ Cả hai: Handle millions concurrent tasks efficiently
✅ Memory: Java virtual threads nhẹ hơn một chút (~1 KB vs ~2 KB)
✅ Syntax: Go cleaner và concise hơn
✅ Ecosystem: Java có larger enterprise ecosystem
✅ Migration: Java dễ (change executor), Go requires rewrite
✅ Use cases: Go cho microservices/cloud-native, Java cho enterprise/legacy


Decision Matrix

Tình HuốngRecommendation
New greenfield projectGo Goroutines
Existing Java codebaseJava Virtual Threads
Microservices architectureGo Goroutines
Enterprise applicationJava Virtual Threads
Team biết JavaJava Virtual Threads
Team học ngôn ngữ mớiGo Goroutines
Cần simple deploymentGo Goroutines
Cần Spring Boot ecosystemJava Virtual Threads
High-concurrency I/OCả hai đều tốt
CPU-intensive tasksGo (slightly faster)

Tài Nguyên Bổ Sung

Go Resources:

Java Resources:


Kết Luận

Cả Go goroutines và Java Virtual Threads đều là excellent solutions để building highly concurrent applications.

Chọn Go nếu bạn coi trọng simplicity, fast deployment, và starting fresh.

Chọn Java nếu bạn có existing Java infrastructure, cần enterprise features, hoặc muốn backward compatibility.

Dù sao, bạn đang building scalable, high-performance applications! 🚀


Có câu hỏi hoặc kinh nghiệm với goroutines hoặc virtual threads? Share trong comments bên dưới!

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.