Java Tutorial

Lesson 3: Multi-Threading

Executors, futures, CompletableFuture, virtual threads, atomic counters, deterministic aggregation, top-N ranking, and ordered parallel results.

Run

scripts/run.sh lesson_3 demo
scripts/run.sh lesson_3 exercise
scripts/apply_answers.sh lesson_3
scripts/run.sh lesson_3 verify
scripts/reset_exercises.sh lesson_3

Function Pages

46 pages generated for Java functions in this lesson.

View related functions

Lesson README

Lesson 3: Multi-Threading

This lesson presents Java concurrency as task orchestration plus shared-state discipline.

120-Minute Plan

MinutesActivity
0-10Compare Java threads, executors, futures, and async pipelines with concurrency tools in other languages.
10-30Run scripts/run.sh lesson_3 demo and identify which code is concurrent.
30-50Read the executor and virtual-thread sections.
50-70Discuss shared mutable state, atomics, and when immutable results are simpler.
70-100Implement the word-statistics exercise functions.
100-115Apply answers and compare sequential and parallel result construction.
115-120Summarize when not to add concurrency.

Exercise

Use workbook.md for the full two-hour learner path. It includes concurrency tracing, staged implementation rounds, and edge-case drills.

Edit src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java.

Functions to implement:

The stubs use simple behavior so learners can run the file before implementing the concurrent solution.

After applying answers, run scripts/run.sh lesson_3 verify to check parallel/sequential consistency, deterministic top-N ordering, exact token matching, stop-word filtering, empty input, and ordered parallel results.

Learner Workbook

Learner Workbook: Lesson 3 Multi-Threading

This workbook is designed to take about two hours. The goal is not to make every line concurrent. The goal is to know where concurrency helps, how Java represents tasks, and how to avoid unsafe shared state.

What You Should Understand By The End

0-15 Minutes: Run And Label Output

Run:

scripts/reset_exercises.sh lesson_3
scripts/run.sh lesson_3 demo
scripts/run.sh lesson_3 exercise

Label each demo output line as one of:

15-35 Minutes: Trace The Demo

Open src/demo/java/tutorial/lesson3/demo/ThreadingDemo.java.

Trace the fixed-pool section:

Then inspect the virtual-thread section. Write one sentence explaining what virtual threads make cheaper, and one sentence explaining what they do not solve.

35-50 Minutes: Read The Exercise API

Open src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java.

Group functions by skill:

Before editing, identify which functions can be implemented without concurrency. Those are good warmups.

50-95 Minutes: Implement In Rounds

Round 1:

Round 2:

Round 3:

Round 4:

Round 5:

Round 6:

Round 7:

95-110 Minutes: Verify Answers

Run:

scripts/apply_answers.sh lesson_3
scripts/run.sh lesson_3 verify
scripts/reset_exercises.sh lesson_3

Read failures carefully. Concurrency bugs often show up as nondeterministic output, mutation of input maps, or wrong ordering.

110-120 Minutes: Edge-Case Drill

Try these cases:

Reset when done:

scripts/reset_exercises.sh lesson_3

Instructor Notes

Instructor Notes: Lesson 3 Multi-Threading

Session Goal

Teach Java concurrency as task coordination plus state discipline. Learners should understand how to run independent work concurrently, collect results, and avoid shared mutable state when aggregation is simpler.

Learning Outcomes

By the end of 120 minutes, learners should be able to:

Before Class

Run:

scripts/reset_exercises.sh lesson_3
scripts/run.sh lesson_3 demo
scripts/run.sh lesson_3 exercise

Confirm javac -version is 21 or newer. Lesson 3 uses Executors.newVirtualThreadPerTaskExecutor().

Know the key files:

Minute-By-Minute Plan

TimeSegmentInstructor Moves
0-10OrientationAsk learners what concurrency tools they know: async/await, goroutines, channels, promises, worker pools, processes.
10-25Run the demoRun scripts/run.sh lesson_3 demo. Identify sequential, fixed-pool, CompletableFuture, virtual-thread, and atomic-counter output.
25-42Fixed thread poolOpen ThreadingDemo.java. Explain submit, Future, get, and executor closure.
42-55CompletableFutureExplain a simple async pipeline and where production code needs error handling.
55-68Virtual threadsExplain that virtual threads make blocking task-per-request style cheaper, but do not remove the need for state safety.
68-78Shared stateDiscuss AtomicInteger and why result aggregation is often clearer than shared mutation.
78-100Exercise workLearners implement the staged concurrency and aggregation lab from workbook.md.
100-112Solution compareApply answers, run the exercise, and compare error handling choices.
112-118Performance discussionAsk when the parallel solution is slower than sequential. Discuss task overhead and input size.
118-120RecapSummarize: isolate work, return values, merge results, close executors, respect interruption.

Teaching Script

Open with:

"Concurrency is not just making code faster. It is deciding what can happen independently and what data must be coordinated."

For the fixed-pool section, trace this sequence:

Key explanation:

"The task is concurrent, but result collection here is deliberately boring. Boring result collection is a feature."

For virtual threads:

"A virtual thread is still a Java thread from the programmer's perspective. The runtime can schedule many of them efficiently, especially when they block. They are not a substitute for thinking about shared state."

Exercise Facilitation

Learners edit ConcurrentStats.java.

Recommended implementation path:

Hints:

Expected Answer Behavior

After applying answers:

scripts/apply_answers.sh lesson_3
scripts/run.sh lesson_3 verify

Expected important lines:

lesson_3 answers verified

Common Mistakes

Discussion Prompts

Extension Challenge

Ask learners to add a stop-word filter for words such as the, and, and are. Then ask whether the filter belongs inside wordsIn, inside each task, or as a separate preprocessing step.

Functions In This Lesson

ConcurrentStats.ConcurrentStats

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:16 | constructor | answer

private ConcurrentStats() {

ConcurrentStats.countWordsSequential

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:19 | method | answer

public static Map<String, Integer> countWordsSequential(List<String> documents) {

ConcurrentStats.countWordsParallel

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:29 | method | answer

public static Map<String, Integer> countWordsParallel(List<String> documents) {

ConcurrentStats.mergeCounts

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:52 | method | answer

public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {

ConcurrentStats.mergeAll

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:58 | method | answer

public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {

ConcurrentStats.totalWordCount

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:66 | method | answer

public static int totalWordCount(Map<String, Integer> counts) {

ConcurrentStats.topWord

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:73 | method | answer

public static Optional<String> topWord(Map<String, Integer> counts) {

ConcurrentStats.topWords

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:77 | method | answer

public static List<String> topWords(Map<String, Integer> counts, int limit) {

ConcurrentStats.countDocumentsContaining

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:91 | method | answer

public static int countDocumentsContaining(List<String> documents, String word) {

ConcurrentStats.countWordsExcludingStopWords

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:105 | method | answer

public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {

ConcurrentStats.documentLengthsParallel

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:120 | method | answer

public static List<Integer> documentLengthsParallel(List<String> documents) {

ConcurrentStats.wordsIn

lesson_3/answers/java/tutorial/lesson3/exercise/ConcurrentStats.java:142 | method | answer

private static List<String> wordsIn(String document) {

ThreadingDemo.ThreadingDemo

lesson_3/src/demo/java/tutorial/lesson3/demo/ThreadingDemo.java:13 | constructor | demo

private ThreadingDemo() {

ThreadingDemo.main

lesson_3/src/demo/java/tutorial/lesson3/demo/ThreadingDemo.java:16 | method | demo

public static void main(String[] args) throws ExecutionException, InterruptedException {

ThreadingDemo.countPrimesUpTo

lesson_3/src/demo/java/tutorial/lesson3/demo/ThreadingDemo.java:63 | method | demo

private static int countPrimesUpTo(int maxInclusive) {

ThreadingDemo.isPrime

lesson_3/src/demo/java/tutorial/lesson3/demo/ThreadingDemo.java:73 | method | demo

private static boolean isPrime(int value) {

AnswerVerifier.AnswerVerifier

lesson_3/src/exercise/java/tutorial/lesson3/exercise/AnswerVerifier.java:8 | constructor | exercise

private AnswerVerifier() {

AnswerVerifier.main

lesson_3/src/exercise/java/tutorial/lesson3/exercise/AnswerVerifier.java:11 | method | exercise

public static void main(String[] args) {

AnswerVerifier.expectEquals

lesson_3/src/exercise/java/tutorial/lesson3/exercise/AnswerVerifier.java:61 | method | exercise

private static void expectEquals(Object expected, Object actual, String label) {

AnswerVerifier.expectThrows

lesson_3/src/exercise/java/tutorial/lesson3/exercise/AnswerVerifier.java:67 | method | exercise

private static void expectThrows(Runnable action, String label) {

ConcurrentStats.ConcurrentStats

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:11 | constructor | exercise

private ConcurrentStats() {

ConcurrentStats.countWordsSequential

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:14 | method | exercise

public static Map<String, Integer> countWordsSequential(List<String> documents) {

ConcurrentStats.countWordsParallel

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:24 | method | exercise

public static Map<String, Integer> countWordsParallel(List<String> documents) {

ConcurrentStats.mergeCounts

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:30 | method | exercise

public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {

ConcurrentStats.mergeAll

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:35 | method | exercise

public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {

ConcurrentStats.totalWordCount

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:40 | method | exercise

public static int totalWordCount(Map<String, Integer> counts) {

ConcurrentStats.topWord

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:45 | method | exercise

public static Optional<String> topWord(Map<String, Integer> counts) {

ConcurrentStats.topWords

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:51 | method | exercise

public static List<String> topWords(Map<String, Integer> counts, int limit) {

ConcurrentStats.countDocumentsContaining

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:57 | method | exercise

public static int countDocumentsContaining(List<String> documents, String word) {

ConcurrentStats.countWordsExcludingStopWords

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:63 | method | exercise

public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {

ConcurrentStats.documentLengthsParallel

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:68 | method | exercise

public static List<Integer> documentLengthsParallel(List<String> documents) {

ConcurrentStats.wordsIn

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java:75 | method | exercise

private static List<String> wordsIn(String document) {

ExerciseRunner.ExerciseRunner

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ExerciseRunner.java:8 | constructor | exercise

private ExerciseRunner() {

ExerciseRunner.main

lesson_3/src/exercise/java/tutorial/lesson3/exercise/ExerciseRunner.java:11 | method | exercise

public static void main(String[] args) {

ConcurrentStats.countWordsSequential

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:14 | method | stub

public static Map<String, Integer> countWordsSequential(List<String> documents) {

ConcurrentStats.countWordsParallel

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:24 | method | stub

public static Map<String, Integer> countWordsParallel(List<String> documents) {

ConcurrentStats.mergeCounts

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:30 | method | stub

public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {

ConcurrentStats.mergeAll

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:35 | method | stub

public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {

ConcurrentStats.totalWordCount

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:40 | method | stub

public static int totalWordCount(Map<String, Integer> counts) {

ConcurrentStats.topWord

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:45 | method | stub

public static Optional<String> topWord(Map<String, Integer> counts) {

ConcurrentStats.topWords

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:51 | method | stub

public static List<String> topWords(Map<String, Integer> counts, int limit) {

ConcurrentStats.countDocumentsContaining

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:57 | method | stub

public static int countDocumentsContaining(List<String> documents, String word) {

ConcurrentStats.countWordsExcludingStopWords

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:63 | method | stub

public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {

ConcurrentStats.documentLengthsParallel

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:68 | method | stub

public static List<Integer> documentLengthsParallel(List<String> documents) {

ConcurrentStats.wordsIn

lesson_3/stubs/java/tutorial/lesson3/exercise/ConcurrentStats.java:75 | method | stub

private static List<String> wordsIn(String document) {