Concurrency
Java threads, synchronized, virtual threads vs C# async/await and Task
Introduction
In this lesson, you'll learn about concurrency in Java. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with java threads, synchronized, virtual threads vs c# async/await and task.
Java has its own approach to java threads, synchronized, virtual threads vs c# async/await and task, which we'll explore step by step.
The Java Way
Let's see how Java handles this concept. Here's a typical example:
import java.util.concurrent.*;
// CompletableFuture — Java's Task<T>
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchSync(url)) // runs on ForkJoin pool
.thenApply(String::toUpperCase) // like .ContinueWith
.exceptionally(ex -> "error: " + ex.getMessage());
String result = future.get(); // block (like await in sync context)
// Await multiple (like Task.WhenAll)
CompletableFuture<Void> all = CompletableFuture.allOf(
CompletableFuture.supplyAsync(() -> fetch(url1)),
CompletableFuture.supplyAsync(() -> fetch(url2))
);
all.get();
// Java 21 Virtual Threads — lightweight like C# async
Thread vt = Thread.ofVirtual().start(() -> {
String data = fetch(url); // blocks virtual thread, not OS thread
process(data);
});
// ExecutorService + Callable (older pattern)
ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor();
Future<String> f = exec.submit(() -> fetch(url));
String r = f.get();
exec.shutdown();
// synchronized (like lock)
private int count = 0;
synchronized void increment() { count++; }
// Or: AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // like Interlocked.IncrementComparing to C#
Here's how you might have written similar code in C#:
using System.Threading.Tasks;
// async/await — non-blocking I/O
public async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
// Parallel.ForEach (CPU-bound parallelism)
Parallel.ForEach(items, item => Process(item));
// Task.WhenAll — await multiple
var tasks = urls.Select(url => FetchAsync(url));
string[] results = await Task.WhenAll(tasks);
// CancellationToken
public async Task WorkAsync(CancellationToken ct)
{
await Task.Delay(1000, ct); // throws if cancelled
}
// Thread-safe counter
private int _count = 0;
Interlocked.Increment(ref _count);
// lock statement
private readonly object _lock = new();
lock (_lock) { _count++; }You may be used to different syntax or behavior.
C# async/await is syntactic sugar over Task; Java uses CompletableFuture chains
You may be used to different syntax or behavior.
Java 21 virtual threads (Thread.ofVirtual()) are Java's equivalent of async I/O without callbacks
You may be used to different syntax or behavior.
synchronized keyword = C# lock statement; AtomicInteger = Interlocked
You may be used to different syntax or behavior.
Task.WhenAll = CompletableFuture.allOf; both take multiple futures and wait for all
You may be used to different syntax or behavior.
C# CancellationToken has no built-in Java equivalent — use volatile boolean or CompletableFuture.cancel()
Step-by-Step Breakdown
1. CompletableFuture vs Task
CompletableFuture is Java's equivalent of C# Task. supplyAsync runs work async; thenApply chains transformations.
public async Task<string> Fetch(string url)
=> await httpClient.GetStringAsync(url);CompletableFuture<String> fetch(String url) {
return CompletableFuture.supplyAsync(() -> {
return httpClient.get(url); // sync call on ForkJoin thread
});
}2. Virtual Threads (Java 21)
Virtual threads (Project Loom) let you write blocking code that runs efficiently — no callback chains needed. Similar to how C# async/await works internally.
// C# async — non-blocking under the hood
await httpClient.GetStringAsync(url);// Java 21 virtual thread — blocking code, lightweight
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
exec.submit(() -> {
String data = httpClient.get(url); // blocking is OK!
process(data);
});
}3. Parallel Execution
CompletableFuture.allOf waits for all futures — like Task.WhenAll. Use it to fan out parallel I/O or CPU work.
string[] results = await Task.WhenAll(
urls.Select(url => FetchAsync(url)));List<CompletableFuture<String>> futures =
urls.stream().map(this::fetchAsync).toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
List<String> results = futures.stream().map(CompletableFuture::join).toList();4. Thread Safety
synchronized blocks/methods = C# lock{}. AtomicInteger = Interlocked. Use concurrent collections from java.util.concurrent.
lock (_lock) { count++; }
Interlocked.Increment(ref count);// synchronized method
synchronized void increment() { count++; }
// AtomicInteger (lock-free)
AtomicInteger count = new AtomicInteger();
count.incrementAndGet(); // thread-safeCommon Mistakes
When coming from C#, developers often make these mistakes:
- C# async/await is syntactic sugar over Task; Java uses CompletableFuture chains
- Java 21 virtual threads (Thread.ofVirtual()) are Java's equivalent of async I/O without callbacks
- synchronized keyword = C# lock statement; AtomicInteger = Interlocked
Key Takeaways
- CompletableFuture replaces Task; supplyAsync = Task.Run; thenApply = ContinueWith
- Java 21 virtual threads let you write blocking code without callback chains
- CompletableFuture.allOf = Task.WhenAll for parallel fan-out
- synchronized = lock; AtomicInteger = Interlocked; java.util.concurrent for concurrent collections