C#
JV

C# to Java

10 lessons

Progress0%
1Introduction2Type Systems3Properties & Getters4Generics5Collections6LINQ to Streams7Async Programming8Ecosystem9Modern Java Features10Concurrency
All Mirror Courses
C#
JV
Concurrency
MirrorLesson 10 of 10
Lesson 10

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.

Mirror Card
C#
From C#:

In C#, you're familiar with java threads, synchronized, virtual threads vs c# async/await and task.

JV
In Java:

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:

JV
Java 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.Increment

Comparing to C#

Here's how you might have written similar code in C#:

C#
C# (What you know)
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++; }
Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

JV
In Java:

C# async/await is syntactic sugar over Task; Java uses CompletableFuture chains

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

JV
In Java:

Java 21 virtual threads (Thread.ofVirtual()) are Java's equivalent of async I/O without callbacks

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

JV
In Java:

synchronized keyword = C# lock statement; AtomicInteger = Interlocked

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

JV
In Java:

Task.WhenAll = CompletableFuture.allOf; both take multiple futures and wait for all

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

JV
In Java:

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.

C#
C#
public async Task<string> Fetch(string url)
    => await httpClient.GetStringAsync(url);
JV
Java
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#
C#
// C# async — non-blocking under the hood
await httpClient.GetStringAsync(url);
JV
Java
// 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.

C#
C#
string[] results = await Task.WhenAll(
    urls.Select(url => FetchAsync(url)));
JV
Java
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.

C#
C#
lock (_lock) { count++; }
Interlocked.Increment(ref count);
JV
Java
// synchronized method
synchronized void increment() { count++; }
// AtomicInteger (lock-free)
AtomicInteger count = new AtomicInteger();
count.incrementAndGet(); // thread-safe

Common 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
Common Pitfall
Don't assume Java works exactly like C#. While the concepts may be similar, the syntax and behavior can differ significantly.

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
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your C# code in Java to practice these concepts.
PreviousFinish