Modern Java

Virtual Threads and Concurrent Collections

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

আগের lessons-এ আমরা Java concurrency-এর foundation শিখেছি:

Thread
Runnable
Callable
ExecutorService
Future
CompletableFuture
synchronized
volatile
Thread Safety

Traditional Java concurrency-তে Platform Thread তুলনামূলক expensive resource।

তাই সাধারণত limited number of threads নিয়ে:

Executors.newFixedThreadPool(...)

ব্যবহার করা হয়।

Modern Java আরেক ধরনের Thread provide করে:

Virtual Thread

Virtual Thread বিশেষভাবে useful যখন application-এ অনেক concurrent task থাকে এবং সেই tasks-এর বড় অংশ সময় কাটে:

waiting
blocking
I/O-এর জন্য অপেক্ষা

এই lesson-এর দ্বিতীয় অংশে আমরা শিখব concurrent collections:

ConcurrentHashMap
CopyOnWriteArrayList
BlockingQueue

কারণ multiple threads একই collection access করলে normal collections সবসময় safe নয়।

এই lesson-এ আমরা শিখব:

  • Platform Thread এবং Virtual Thread
  • Virtual Thread কেন useful
  • Virtual Thread কোথায় useful নয়
  • Thread.startVirtualThread()
  • Thread.ofVirtual()
  • Executors.newVirtualThreadPerTaskExecutor()
  • Task-per-thread model
  • কেন Virtual Threads pool করা হয় না
  • Resource concurrency limit
  • Semaphore
  • Virtual Threads এবং thread safety
  • ConcurrentHashMap
  • putIfAbsent()
  • computeIfAbsent()
  • CopyOnWriteArrayList
  • BlockingQueue
  • ArrayBlockingQueue
  • Producer-consumer pattern
  • Backpressure
  • Thread-safe collection বনাম thread-safe workflow

Platform Thread

Traditional Java Thread:

Thread worker =
        new Thread(
                () ->
                        doWork()
        );

একটি:

Platform Thread

create করে।

Platform Thread operating system thread-এর সাথে closely connected।

Conceptually:

Java Platform Thread
↓
Operating System Thread
↓
CPU

Why Platform Threads Are Limited

Operating System threads resource consume করে।

Examples:

memory
thread stack
OS scheduling
context switching
runtime bookkeeping

তাই application-এ arbitrary সংখ্যক Platform Threads create করা practical নয়।


Traditional Thread Pool

এই কারণে traditional concurrency-তে:

ExecutorService executor =
        Executors.newFixedThreadPool(
                10
        );

ব্যবহার করা হয়।

Conceptually:

1000 Tasks
↓
Task Queue
↓
10 Platform Threads

একটি worker task শেষ করলে পরের task নেয়।


Why Pool Platform Threads?

Mental model:

Platform Threads expensive
↓
few threads create করো
↓
reuse করো

এটি Platform Thread-এর জন্য sensible strategy।


What Is a Virtual Thread?

Virtual Thread-ও Java:

Thread

কিন্তু Virtual Thread পুরো lifetime একটি নির্দিষ্ট Operating System Thread ধরে রাখে না।

Java runtime Virtual Threads schedule করতে পারে।

Conceptually:

Many Virtual Threads
↓
Java Runtime
↓
Carrier Platform Threads
↓
Operating System

The Important Difference

Suppose একটি task:

কিছু Java code execute করল
↓
I/O-এর জন্য wait করল
↓
response এলো
↓
আবার Java code execute করল

Platform Thread waiting-এর সময় underlying OS Thread occupy করতে পারে।

Virtual Thread-এর ক্ষেত্রে runtime অনেক blocking situation-এ Virtual Thread suspend করে underlying execution resource অন্য task-এর জন্য ব্যবহার করতে পারে।

এটাই high-concurrency blocking workload-এর জন্য Virtual Threads powerful করে।


Virtual Threads Are Not Faster Threads

এই distinction খুব important।

Virtual Thread:

একটি CPU calculation দ্রুত করে না।

Suppose একটি calculation-এর CPU work:

500 ms

Virtual Thread ব্যবহার করলেই সেটি:

50 ms

হয়ে যাবে না।

Virtual Thread-এর goal:

Scale

not:

Make one task faster

Throughput vs Latency

Two different concepts:

Throughput
→ একটি সময়ের মধ্যে কত কাজ handle করা যাচ্ছে

Latency
→ একটি individual কাজ শেষ হতে কত সময় লাগছে

Virtual Threads অনেক waiting-heavy task concurrently handle করতে সাহায্য করতে পারে।

এটি individual operation-এর latency automatically কমায় না।


Good Virtual Thread Workloads

Virtual Threads strong candidate যখন tasks:

high in number
mostly independent
frequently blocking
spend significant time waiting

Examples conceptually:

network I/O
file I/O
many request-style operations
blocking queue operations
external service waiting

CPU-Bound Work

Examples:

large mathematical calculation
compression
image transformation
large parsing workload
cryptographic computation

এগুলো CPU-bound হতে পারে।

If machine has:

8 CPU cores

Virtual Threads:

10,000 CPU cores

create করে না।

CPU-bound workload-এর parallelism এখনও hardware capacity দ্বারা limited।


Creating a Virtual Thread

Simple API:

Thread worker =
        Thread.startVirtualThread(
                () ->
                        System.out.println(
                                "Running in a virtual thread"
                        )
        );

এটি Virtual Thread create এবং start করে।


Check Thread Type

System.out.println(
        worker.isVirtual()
);

Result:

true

Virtual Thread Builder

Another API:

Thread worker =
        Thread.ofVirtual()
                .start(
                        () ->
                                doWork()
                );

Named Virtual Thread

Thread worker =
        Thread.ofVirtual()
                .name(
                        "course-worker"
                )
                .start(
                        () ->
                                doWork()
                );

Thread names debugging এবং observability-তে useful।


Virtual-Thread-Per-Task Executor

Application code-এ manually every Thread create না করে Executor ব্যবহার করা যায়:

ExecutorService executor =
        Executors.newVirtualThreadPerTaskExecutor();

প্রতিটি submitted task-এর জন্য একটি নতুন Virtual Thread তৈরি হয়।

Conceptually:

Task 1
→ Virtual Thread 1

Task 2
→ Virtual Thread 2

Task 3
→ Virtual Thread 3

Task-Per-Thread Model

Platform Thread pool mental model:

Threads are workers.
Tasks borrow workers.

Virtual Thread mental model:

One task
≈
One Virtual Thread

Virtual Thread task-এর lifetime represent করতে পারে।


Example

import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {

    public static void main(
            String[] args
    ) throws Exception {
        try (
                var executor =
                        Executors
                                .newVirtualThreadPerTaskExecutor()
        ) {
            Future<String> java =
                    executor.submit(
                            () ->
                                    loadCourse(
                                            "JAVA"
                                    )
                    );

            Future<String> backend =
                    executor.submit(
                            () ->
                                    loadCourse(
                                            "BACKEND"
                                    )
                    );

            System.out.println(
                    java.get()
            );

            System.out.println(
                    backend.get()
            );
        }
    }

    static String loadCourse(
            String code
    ) {
        return "Loaded "
                + code;
    }
}

Why Try-With-Resources?

ExecutorService resource lifecycle manage করতে হয়।

Modern Java-তে:

try (
        var executor =
                Executors
                        .newVirtualThreadPerTaskExecutor()
) {
    ...
}

executor lifecycle block-এর সাথে clearly bound করা যায়।


Do Not Pool Virtual Threads

Traditional logic:

Platform Thread expensive
→ pool them

Virtual Thread logic:

Virtual Thread lightweight
→ create one per task

তাই:

fixed pool of 20 Virtual Threads

create করা সাধারণত Virtual Thread model-এর উদ্দেশ্য নয়।


But Does This Mean Unlimited Work?

No।

এটি important।

Virtual Threads cheap হওয়া মানে system-এর সব resource unlimited নয়।

Still limited:

CPU
memory
database connections
network connections
file descriptors
downstream service capacity
queue capacity

So:

many Virtual Threads allowed

does not mean:

unlimited work allowed

Thread Limit vs Resource Limit

Suppose:

5000 Virtual Threads

একটি downstream resource call করছে।

কিন্তু সেই resource safely handle করতে পারে:

20 concurrent operations

আমাদের limit করা উচিত:

resource access

not Virtual Thread creation itself।


Semaphore

Java provides:

Semaphore

যেটি concurrent access limit করতে পারে।

Example:

Semaphore permits =
        new Semaphore(
                20
        );

Meaning:

maximum 20 tasks
protected section-এ একই সময়ে ঢুকতে পারবে।

Semaphore Example

import java.util.concurrent.Semaphore;

final class LimitedService {

    private final Semaphore permits =
            new Semaphore(
                    10
            );

    String call(
            String value
    ) throws InterruptedException {
        permits.acquire();

        try {
            return performCall(
                    value
            );
        } finally {
            permits.release();
        }
    }

    private String performCall(
            String value
    ) {
        return "Processed "
                + value;
    }
}

Flow:

acquire permit
↓
perform limited operation
↓
release permit

Why finally Matters

If:

performCall(...)

throws exception, permit still release করতে হবে।

Therefore:

finally {
    permits.release();
}

important।

Otherwise permits leak হতে পারে এবং eventually no task can proceed।


Virtual Thread + Semaphore

Many Virtual Threads থাকতে পারে:

Task 1
Task 2
...
Task 10000

but Semaphore limit করতে পারে:

Only 10 call this resource simultaneously

This cleanly separates:

Task concurrency

from:

Resource capacity

Virtual Threads Do Not Remove Race Conditions

Suppose:

int count;

Then many Virtual Threads:

count++;

execute করছে।

Same problem remains:

read
modify
write

count++ still not atomic।

Virtual Threads thread cost change করে।

তারা Java Memory Model change করে না।


Same Thread-Safety Rules Apply

Virtual Threads ব্যবহার করলেও:

shared mutable state
race conditions
visibility
atomicity
locks
immutability

সব concepts relevant থাকে।

Example:

ArrayList

Virtual Thread ব্যবহার করলেই concurrent collection হয়ে যায় না।


Virtual Threads and Synchronous Code

Virtual Threads-এর বড় advantage হলো waiting-heavy workflows simple blocking style-এ লেখা যায়।

Example:

Course course =
        loadCourse();

List<Lesson> lessons =
        loadLessons(
                course
        );

process(
        lessons
);

এই synchronous structure অনেক সময়:

easy to read
easy to debug
easy to reason about

হতে পারে।


What About CompletableFuture?

Virtual Threads CompletableFuture obsolete করে না।

They solve different concerns।

CompletableFuture strong যখন workflow:

multiple async computations compose করা
independent results combine করা
completion pipeline বানানো

Virtual Threads strong যখন:

many blocking tasks
simple sequential code
task-per-thread model

Example Decision

Sequential dependency:

Load Course
↓
Load Lessons
↓
Generate Result

Virtual Thread-এর ভিতরে ordinary blocking code very readable হতে পারে।

Independent operations:

Load Course ─┐
             ├→ Combine
Load Stats ──┘

CompletableFuture.thenCombine() useful হতে পারে।

Use the abstraction that best communicates the workflow।


Concurrent Collections

Now suppose multiple threads genuinely একই collection share করছে।

Normal collections:

HashMap
ArrayList
HashSet
ArrayDeque

excellent general-purpose data structures।

কিন্তু arbitrary concurrent mutation-এর জন্য এগুলো automatically safe নয়।

Java তাই concurrency-aware collections provide করে।


Important Rule

Thread-safe collection
≠
Thread-safe business workflow

Collection-এর individual operations safe হতে পারে।

কিন্তু multiple operations combine করে business logic race করতে পারে।


ConcurrentHashMap

Concurrent key-value access-এর জন্য:

ConcurrentHashMap<K, V>

Example:

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Course> courses =
        new ConcurrentHashMap<>();

Basic Operations

Insert:

courses.put(
        course.code(),
        course
);

Lookup:

Course course =
        courses.get(
                "JAVA"
        );

Remove:

courses.remove(
        "JAVA"
);

Check-Then-Act Problem

Suppose requirement:

Course code absent হলে insert করো।

Naive:

if (
        !courses.containsKey(
                course.code()
        )
) {
    courses.put(
            course.code(),
            course
    );
}

Problem:

containsKey()

and:

put()

two separate operations।

Thread A এবং Thread B দুজনেই:

missing

observe করতে পারে before either inserts।


putIfAbsent()

Better:

Course existing =
        courses.putIfAbsent(
                course.code(),
                course
        );

This expresses:

insert only if missing

as one concurrent Map operation।


Registration Example

boolean register(
        Course course
) {
    Course existing =
            courses.putIfAbsent(
                    course.code(),
                    course
            );

    return existing
            == null;
}

If absent:

inserted
→ true

If already exists:

not replaced
→ false

computeIfAbsent()

Suppose missing value হলে create করতে হবে।

Example:

CourseStatistics statistics =
        statisticsByCourse
                .computeIfAbsent(
                        courseCode,
                        ignored ->
                                new CourseStatistics()
                );

Meaning:

value আছে
→ existing return

value নেই
→ create and associate value

Why This Is Useful

Instead of:

CourseStatistics stats =
        map.get(
                code
        );

if (
        stats == null
) {
    stats =
            new CourseStatistics();

    map.put(
            code,
            stats
    );
}

use:

map.computeIfAbsent(
        code,
        ignored ->
                new CourseStatistics()
);

when that matches your intent।


Keep Computation Small

Avoid huge operation inside:

computeIfAbsent(...)

Example of poor direction:

map.computeIfAbsent(
        code,
        key -> {
            // external calls
            // long calculations
            // unrelated mutations
            // many business operations
        }
);

The computation should remain focused।


ConcurrentHashMap and null

ConcurrentHashMap does not use null as normal keys or values।

This helps methods such as:

get()

use:

null

to clearly mean:

no mapping

Thread-Safe Map Does Not Make Values Thread-Safe

Suppose:

ConcurrentHashMap<
        String,
        ArrayList<String>
> lessons =
        new ConcurrentHashMap<>();

The outer Map is concurrency-aware।

But:

ArrayList<String>

values are still ordinary mutable Lists।

Two threads mutating the same List can still race।


Example

lessons.computeIfAbsent(
        "JAVA",
        ignored ->
                new ArrayList<>()
).add(
        "Streams"
);

The computeIfAbsent() operation is map-safe।

The subsequent:

add()

acts on a normal ArrayList

Outer collection safety does not automatically propagate into nested objects।


Prefer Immutable Values Where Possible

Example:

ConcurrentHashMap<
        String,
        CourseSummary
> courses =
        new ConcurrentHashMap<>();

If CourseSummary is deeply immutable:

concurrent reads become easier to reason about

because threads are not mutating shared CourseSummary state।


Atomic Map Operation Is Not a Transaction

Suppose business operation touches:

courses Map
+
enrollments Map
+
capacity

Using ConcurrentHashMap for each does not automatically make:

check capacity
+
create enrollment
+
update count

atomic।

The business invariant spans multiple pieces of state।

You still need an appropriate higher-level concurrency strategy।


CopyOnWriteArrayList

Java provides:

CopyOnWriteArrayList<E>

A concurrent List implementation।

Example:

CopyOnWriteArrayList<String> listeners =
        new CopyOnWriteArrayList<>();

How Copy-on-Write Works

High-level mental model:

Read
→ existing snapshot

Write
→ underlying array copied
→ modification applied to new copy

Therefore writes are comparatively expensive।


Good Workload

CopyOnWriteArrayList useful when:

reads are very frequent
iteration is frequent
writes are rare
collection is relatively small

Example:

event listeners
callbacks
small configuration subscribers

Poor Workload

Avoid choosing it automatically for:

large write-heavy collection

because every mutation may involve copying array state।


Snapshot-Style Iteration

Suppose iterator create হলো।

Another thread পরে:

listeners.add(
        "audit"
);

Existing iterator নতুন value নাও দেখতে পারে।

Iterator একটি stable snapshot-style view observe করে।

This is intentional।


Example

import java.util.concurrent.CopyOnWriteArrayList;

final class ListenerRegistry {

    private final CopyOnWriteArrayList<String> listeners =
            new CopyOnWriteArrayList<>();

    void add(
            String listener
    ) {
        listeners.addIfAbsent(
                listener
        );
    }

    void notifyListeners() {
        for (
                String listener
                : listeners
        ) {
            System.out.println(
                    "Notify "
                    + listener
            );
        }
    }
}

Good fit when registration rare but notification traversal frequent।


BlockingQueue

Producer-consumer workflow-এর জন্য:

BlockingQueue<E>

একটি powerful abstraction।

It extends Queue semantics with operations that can:

wait for data
wait for capacity

Producer-Consumer Model

Conceptually:

Producer
↓
BlockingQueue
↓
Consumer

Producer work তৈরি করে।

Consumer work process করে।

Queue handoff coordinate করে।


Why Not Busy Poll?

Without blocking support, consumer may do:

while (
        true
) {
    Task task =
            queue.poll();

    if (
            task == null
    ) {
        continue;
    }

    process(
            task
    );
}

Queue empty হলে loop repeatedly execute হয়।

এটি unnecessary CPU consume করতে পারে।


take()

BlockingQueue:

Task task =
        queue.take();

If queue empty:

wait

until element becomes available।

No busy polling needed।


put()

For bounded BlockingQueue:

queue.put(
        task
);

If queue full:

wait

until space becomes available।


Main Operation Families

Immediate attempt:

offer(
        value
)

Wait for space:

put(
        value
)

Immediate removal:

poll()

Wait for data:

take()

Mental model:

offer()
→ try now

put()
→ wait for capacity

poll()
→ try now

take()
→ wait for data

ArrayBlockingQueue

A bounded FIFO BlockingQueue implementation:

BlockingQueue<String> queue =
        new ArrayBlockingQueue<>(
                100
        );

Capacity:

100

Once queue is full:

put(...)

waits।

If queue empty:

take()

waits।


Backpressure

Suppose producer can generate:

10,000 tasks/second

but consumer can process:

1,000 tasks/second

If queue grows without meaningful limits:

queue grows
↓
memory usage grows
↓
latency grows
↓
system becomes unhealthy

A bounded queue can push pressure back to producer।

When full:

put()

must wait।

This is a simple form of:

backpressure

Complete Producer-Consumer Example

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Main {

    public static void main(
            String[] args
    ) throws InterruptedException {
        BlockingQueue<String> queue =
                new ArrayBlockingQueue<>(
                        2
                );

        Thread producer =
                Thread.startVirtualThread(
                        () ->
                                produce(
                                        queue
                                )
                );

        Thread consumer =
                Thread.startVirtualThread(
                        () ->
                                consume(
                                        queue
                                )
                );

        producer.join();
        consumer.join();
    }

    static void produce(
            BlockingQueue<String> queue
    ) {
        try {
            for (
                    int i = 1;
                    i <= 5;
                    i++
            ) {
                String task =
                        "Task-"
                        + i;

                queue.put(
                        task
                );

                System.out.println(
                        "Produced "
                        + task
                );
            }
        } catch (
                InterruptedException exception
        ) {
            Thread.currentThread()
                    .interrupt();
        }
    }

    static void consume(
            BlockingQueue<String> queue
    ) {
        try {
            for (
                    int i = 1;
                    i <= 5;
                    i++
            ) {
                String task =
                        queue.take();

                System.out.println(
                        "Consumed "
                        + task
                );
            }
        } catch (
                InterruptedException exception
        ) {
            Thread.currentThread()
                    .interrupt();
        }
    }
}

Why Capacity 2 Matters

Producer can add:

Task-1
Task-2

Queue becomes full।

Before another:

put(...)

can succeed, consumer needs to remove something।

This creates natural coordination।


BlockingQueue Does Not Accept null

BlockingQueue uses:

null

as a special absence result for methods such as:

poll()

Therefore normal queue elements cannot be null


Do Not Check Before take()

Avoid:

if (
        !queue.isEmpty()
) {
    return queue.take();
}

The queue can change between:

isEmpty()

and:

take()

If desired semantics are:

wait for an item

simply use:

take()

Use Semantic Atomic Operations

Concurrency APIs often already provide an operation representing the whole intent।

Instead of:

if (
        !map.containsKey(
                key
        )
) {
    map.put(
            key,
            value
    );
}

use:

map.putIfAbsent(
        key,
        value
);

Instead of:

if (
        !queue.isEmpty()
) {
    queue.poll();
}

just:

queue.poll();

if immediate attempt is what you mean।


Thread-Safe Collection vs Thread-Safe Workflow

Consider:

ConcurrentHashMap<String, Course> courses;

This operation:

courses.putIfAbsent(...)

has concurrent Map semantics।

But this workflow:

Check Course exists
↓
Check seat capacity
↓
Create Enrollment
↓
Update another structure

spans multiple pieces of state।

A concurrent Map cannot automatically make the whole workflow atomic।


Protect the Business Invariant

Concurrency reasoning should ask:

What must remain true?

For example:

Only 100 learners may enroll.

The invariant is not:

Map.put() must be thread-safe.

It is:

Enrollment count must never exceed 100.

The whole state transition needs a strategy capable of protecting that rule।


Tool-to-Problem Mapping

Virtual Thread
→ represent many concurrent tasks

ExecutorService
→ execute/manage tasks

Semaphore
→ limit access to scarce resource

ConcurrentHashMap
→ concurrent key-value operations

CopyOnWriteArrayList
→ read-heavy concurrent List

BlockingQueue
→ producer-consumer handoff

synchronized
→ protect explicit critical section

Immutability
→ reduce shared mutable state

Different tools solve different problems।


Common Mistake 1 — Virtual Threads Make Everything Faster

False।

They mainly help scalability for many waiting/blocking tasks।


Common Mistake 2 — Pooling Virtual Threads

Traditional Platform Thread pool patterns should not automatically be applied to Virtual Threads।

Prefer:

Executors.newVirtualThreadPerTaskExecutor()

for task-per-thread usage।


Common Mistake 3 — Unlimited Tasks

Virtual Threads are lightweight, but:

work
memory
CPU
connections
downstream capacity

remain finite।

Workload control still matters।


Common Mistake 4 — Virtual Threads Remove Race Conditions

They do not।

This remains unsafe:

count++;

when shared concurrently।


Common Mistake 5 — Using Thread Count to Protect a Resource

If database or external service capacity is:

20

model that resource limit explicitly, for example with:

Semaphore

when appropriate।


Common Mistake 6 — HashMap for Concurrent Mutation

If Map is deliberately shared across multiple writers, use appropriate concurrency control or a concurrent implementation।


Common Mistake 7 — Manual Check-Then-Act

Avoid:

containsKey()
then
put()

when:

putIfAbsent()

matches the requirement।


Common Mistake 8 — Expensive computeIfAbsent()

Keep mapping function:

short
focused
side-effect controlled

Do not hide a large business workflow inside it।


Common Mistake 9 — CopyOnWriteArrayList for Heavy Writes

It is designed around expensive writes and cheap/stable reads।

Write-heavy workloads need another strategy।


Common Mistake 10 — Busy Polling

Avoid repeatedly:

poll()

inside a tight empty loop when what you really want is:

take()

and wait।


Common Mistake 11 — Ignoring Interruption

Operations such as:

Semaphore.acquire()
BlockingQueue.put()
BlockingQueue.take()

may throw:

InterruptedException

Handle interruption deliberately।


Common Mistake 12 — Concurrent Outer Collection Means Everything Safe

A:

ConcurrentHashMap<String, ArrayList<String>>

still contains mutable ArrayList values।

Nested state requires its own thread-safety reasoning।


Practical Example — Concurrent Course Registry

import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public final class CourseRegistry {

    private final ConcurrentHashMap<String, Course> courses =
            new ConcurrentHashMap<>();

    public boolean register(
            Course course
    ) {
        Course existing =
                courses.putIfAbsent(
                        course.code(),
                        course
                );

        return existing
                == null;
    }

    public Optional<Course> find(
            String code
    ) {
        return Optional.ofNullable(
                courses.get(
                        code
                )
        );
    }

    public record Course(
            String code,
            String title
    ) {
    }
}

Important operation:

putIfAbsent()

captures:

register only when code doesn't already exist

without manual check-then-act।


Practical Example — Read-Heavy Listener Registry

import java.util.concurrent.CopyOnWriteArrayList;

public final class ListenerRegistry {

    private final CopyOnWriteArrayList<String> listeners =
            new CopyOnWriteArrayList<>();

    public void register(
            String listener
    ) {
        listeners.addIfAbsent(
                listener
        );
    }

    public void notifyListeners() {
        for (
                String listener
                : listeners
        ) {
            System.out.println(
                    "Notify "
                    + listener
            );
        }
    }
}

This is reasonable when:

registration rare
notification iteration frequent

Practical Example — Bounded Work Queue

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public final class WorkQueue {

    private final BlockingQueue<String> tasks =
            new ArrayBlockingQueue<>(
                    100
            );

    public void submit(
            String task
    ) throws InterruptedException {
        tasks.put(
                task
        );
    }

    public String next()
            throws InterruptedException {
        return tasks.take();
    }
}

API tells us:

submit()
→ may wait when full

next()
→ may wait when empty

Practice 1 — Virtual or Platform Thread?

There may be thousands of independent tasks that mostly wait on blocking operations।

Which modern Thread model should you consider?

Answer

Virtual Threads

are a strong candidate।


Practice 2 — CPU-Bound Task

Will 100,000 Virtual Threads make a heavy CPU calculation 100,000 times faster?

Answer

No।

CPU remains limited by available hardware।


Practice 3 — Start Virtual Thread

Solution

Thread worker =
        Thread.startVirtualThread(
                () ->
                        doWork()
        );

Practice 4 — Per-Task Executor

Solution

var executor =
        Executors
                .newVirtualThreadPerTaskExecutor();

Practice 5 — Pool Virtual Threads?

Should Virtual Threads normally be treated like scarce Platform Threads and reused through a small fixed pool?

Answer

No।

Task-per-Virtual-Thread is the intended model।


Practice 6 — Resource Capacity

Thousands of tasks exist but only 10 may access a resource simultaneously।

What can model the limit?

Answer

A:

Semaphore

can be appropriate।


Practice 7 — Race Condition

Does Virtual Thread make this safe?

count++;

Answer

No।


Practice 8 — Concurrent Map

Need concurrent key-value registry।

Answer

Consider:

ConcurrentHashMap

Practice 9 — Insert If Missing

Answer

putIfAbsent()

Practice 10 — Create If Missing

Answer

computeIfAbsent()

Practice 11 — Read-Heavy List

Reads and iterations are very frequent, mutations very rare।

Answer

Consider:

CopyOnWriteArrayList

Practice 12 — Write-Heavy List

Should CopyOnWriteArrayList automatically be chosen?

Answer

No।

Copy-on-write mutation can be expensive।


Practice 13 — Consumer Waits for Work

Answer

queue.take();

Practice 14 — Producer Waits for Space

On a bounded BlockingQueue:

queue.put(
        task
);

Practice 15 — Whole Workflow

Does ConcurrentHashMap make a multi-step operation involving several objects automatically atomic?

Answer

No।

The complete business invariant still needs correct concurrency design।


True or False

  1. Virtual Thread is still a Java Thread.
  2. Virtual Threads are mainly useful for high-concurrency waiting-heavy workloads.
  3. Virtual Threads make CPU calculations intrinsically faster.
  4. Thread.startVirtualThread() starts a Virtual Thread.
  5. newVirtualThreadPerTaskExecutor() creates a Virtual Thread for each task.
  6. Virtual Threads should normally be pooled as scarce workers.
  7. Semaphore can limit concurrent access to a resource.
  8. Virtual Threads eliminate race conditions.
  9. ConcurrentHashMap provides concurrent Map operations.
  10. putIfAbsent() can avoid manual insert check-then-act.
  11. CopyOnWriteArrayList is best for every write-heavy workload.
  12. BlockingQueue.take() may wait until an element exists.
  13. BlockingQueue.put() may wait for capacity.
  14. A bounded queue can provide backpressure.
  15. Thread-safe collections automatically make every business workflow thread-safe.

Answers

1. True
2. True
3. False
4. True
5. True
6. False
7. True
8. False
9. True
10. True
11. False
12. True
13. True
14. True
15. False

Knowledge Check

Question 1

Platform Thread এবং Virtual Thread-এর main conceptual difference কী?

Question 2

Virtual Threads কোন ধরনের workload-এর জন্য particularly useful?

Question 3

কেন Virtual Threads CPU-intensive code automatically faster করে না?

Question 4

Task-per-thread model কী?

Question 5

কেন Virtual Threads pool করা সাধারণত দরকার হয় না?

Question 6

Resource concurrency কীভাবে Virtual Thread count থেকে separately limit করা যায়?

Question 7

Virtual Threads কি thread-safety rules change করে?

Question 8

ConcurrentHashMap কেন useful?

Question 9

putIfAbsent() কেন manual containsKey() + put() থেকে safer?

Question 10

computeIfAbsent() কী problem solve করে?

Question 11

CopyOnWriteArrayList কোন workload-এর জন্য suited?

Question 12

BlockingQueue normal Queue-এর চেয়ে কী extra capability দেয়?

Question 13

put() এবং take() কী করে?

Question 14

Backpressure কীভাবে bounded queue-এর মাধ্যমে তৈরি হয়?

Question 15

Thread-safe collection এবং thread-safe workflow-এর difference কী?


Knowledge Check Answers

Answer 1

Platform Thread Operating System thread-এর সাথে closely connected এবং তুলনামূলক expensive resource।

Virtual Thread runtime-managed lightweight Thread, যা many concurrent tasks represent করতে পারে এবং waiting-এর সময় একটি specific OS Thread permanently occupy করতে হয় না।

Answer 2

যখন application-এ অনেক concurrent tasks আছে এবং তারা significant সময় blocking/waiting-এ কাটায়।

Answer 3

Virtual Threads CPU cores বাড়ায় না।

CPU-intensive computation hardware processing capacity দ্বারাই limited।

Answer 4

প্রতিটি concurrent task-এর জন্য একটি separate Virtual Thread ব্যবহার করা।

Conceptually:

Task
→ Virtual Thread

Answer 5

Traditional pooling scarce Platform Threads reuse করার জন্য প্রয়োজন।

Virtual Threads lightweight এবং task lifetime represent করার জন্য designed।

Answer 6

Scarce resource explicitly limit করা যায়।

Example:

Semaphore

যখন requirement হলো maximum concurrent access control করা।

Answer 7

No।

Shared state, race condition, atomicity, visibility, synchronization—সব rules relevant থাকে।

Answer 8

Multiple threads-এর concurrent key-value access-এর জন্য concurrency-aware Map semantics provide করে।

Answer 9

Manual:

check absent
then insert

দুইটি separate operation।

putIfAbsent() সেই intent এক concurrent operation-এ express করে।

Answer 10

Key-এর mapping না থাকলে value compute এবং associate করতে সাহায্য করে।

Answer 11

যেখানে:

reads and traversals frequent
writes rare

এবং snapshot-style iteration acceptable।

Answer 12

Queue operations প্রয়োজন হলে wait করতে পারে:

data-এর জন্য
বা
capacity-এর জন্য

Answer 13

put()

bounded queue full হলে space-এর জন্য wait করতে পারে।

take()

queue empty হলে element-এর জন্য wait করতে পারে।

Answer 14

Queue full হলে producer আর unlimited rate-এ values add করতে পারে না।

put() অপেক্ষা করে, ফলে producer-এর উপর pressure ফিরে যায়।

Answer 15

Thread-safe collection তার own operations-এর concurrency guarantees দেয়।

Business workflow multiple collections, objects এবং conditions involve করতে পারে।

সেই full invariant automatically atomic হয় না।


Practical Selection Guide

Many waiting-heavy tasks:

Virtual Thread

Per-task Virtual Thread execution:

Executors.newVirtualThreadPerTaskExecutor()

Limit scarce resource access:

Semaphore

Concurrent key-value state:

ConcurrentHashMap

Atomic insert-if-missing:

putIfAbsent()

Compute value if missing:

computeIfAbsent()

Read-heavy, rarely modified List:

CopyOnWriteArrayList

Producer-consumer handoff:

BlockingQueue

Bounded FIFO queue:

ArrayBlockingQueue

Protect explicit shared invariant:

synchronized

Avoid shared mutation where possible:

Immutability
Thread confinement
Stateless design

Core Mental Model

Modern Java concurrency-তে concerns আলাদা করে ভাবুন।

How do I represent many tasks?
→ Virtual Threads

How do I execute tasks?
→ ExecutorService

How many tasks may access a scarce resource?
→ Semaphore / resource capacity

How do tasks exchange work?
→ BlockingQueue

How do threads share key-value data?
→ ConcurrentHashMap

How do I safely share read-heavy lists?
→ CopyOnWriteArrayList

How do I protect a business invariant?
→ Appropriate synchronization/state design

Virtual Threads-এর সবচেয়ে important mental shift:

Platform Thread model:
Threads are scarce workers.

Virtual Thread model:
A thread can represent a task.

কিন্তু:

Cheap threads
≠
Unlimited system capacity

CPU, memory, connections, queue capacity এবং downstream systems এখনও finite।


Lesson Summary

এই lesson-এ আমরা Virtual Threads এবং Concurrent Collections-এর foundation শিখেছি।

আমরা শিখেছি:

  • Platform Threads তুলনামূলক expensive execution resources
  • Traditional thread pools Platform Threads reuse করে
  • Virtual Threads lightweight Java Threads
  • Virtual Threads high-concurrency blocking/waiting workloads-এর জন্য particularly useful
  • Virtual Threads single CPU task automatically faster করে না
  • Virtual Threads scale/throughput improve করতে পারে, CPU speed নয়
  • Thread.startVirtualThread() Virtual Thread start করতে পারে
  • Thread.ofVirtual() builder API provide করে
  • newVirtualThreadPerTaskExecutor() প্রতিটি task-এর জন্য Virtual Thread create করে
  • Virtual Threads task-per-thread model support করে
  • Virtual Threads traditional fixed worker pool হিসেবে treat করা উচিত নয়
  • Cheap Virtual Threads unlimited resources imply করে না
  • Semaphore scarce resource concurrency independently limit করতে পারে
  • Virtual Threads race condition বা shared-state problem remove করে না
  • ConcurrentHashMap concurrent key-value access-এর জন্য designed
  • putIfAbsent() insert-if-missing operation atomicভাবে express করতে পারে
  • computeIfAbsent() missing values create করতে useful
  • Concurrent Map-এর mutable values-এর জন্য আলাদা thread-safety reasoning দরকার
  • CopyOnWriteArrayList read-heavy, write-light workloads-এর জন্য useful
  • Copy-on-write writes relatively expensive
  • BlockingQueue producer-consumer coordination support করে
  • put() capacity-এর জন্য wait করতে পারে
  • take() data-এর জন্য wait করতে পারে
  • ArrayBlockingQueue bounded FIFO work queue
  • Bounded queues backpressure তৈরি করতে পারে
  • Concurrent collection-এর individual operation safe হলেও entire business workflow automatically atomic নয়
  • Concurrency correctness business invariant-এর level-এ reason করতে হয়

সবচেয়ে important principle:

Virtual Threads concurrency cheap করে।

Concurrent Collections shared data access সহজ করে।

কিন্তু correctness এখনো
state ownership,
resource limits,
and business invariants-এর
উপর নির্ভর করে।

Next Lesson

পরবর্তী lesson:

Module Practice and Assessment

এই final Module 7 assessment-এ আমরা পুরো Modern Java module review করব:

  • Lambda Expressions
  • Functional Interfaces
  • Method References
  • Stream API
  • Filtering and Transformation
  • flatMap()
  • Collectors
  • Grouping and Reduction
  • Optional
  • Modern Date and Time API
  • var
  • Switch Expressions
  • Pattern Matching
  • Records
  • Concurrency
  • Thread Safety
  • Executors
  • Future
  • CompletableFuture
  • Virtual Threads
  • Concurrent Collections

Final assessment-এ theory-এর পাশাপাশি integrated practical Java exercises থাকবে।