ConcurrentStats.ConcurrentStats
private ConcurrentStats() {
Executors, futures, CompletableFuture, virtual threads, atomic counters, deterministic aggregation, top-N ranking, and ordered parallel results.
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
46 pages generated for Java functions in this lesson.
This lesson presents Java concurrency as task orchestration plus shared-state discipline.
| Minutes | Activity |
|---|---|
| 0-10 | Compare Java threads, executors, futures, and async pipelines with concurrency tools in other languages. |
| 10-30 | Run scripts/run.sh lesson_3 demo and identify which code is concurrent. |
| 30-50 | Read the executor and virtual-thread sections. |
| 50-70 | Discuss shared mutable state, atomics, and when immutable results are simpler. |
| 70-100 | Implement the word-statistics exercise functions. |
| 100-115 | Apply answers and compare sequential and parallel result construction. |
| 115-120 | Summarize when not to add concurrency. |
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:
countWordsParallelmergeCountsmergeAlltotalWordCounttopWordtopWordscountDocumentsContainingcountWordsExcludingStopWordsdocumentLengthsParallelThe 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.
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.
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:
CompletableFutureOpen 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.
Open src/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java.
Group functions by skill:
countWordsSequential, countWordsParallel, countWordsExcludingStopWordsmergeCounts, mergeAlltopWord, topWordscountDocumentsContainingdocumentLengthsParallelBefore editing, identify which functions can be implemented without concurrency. Those are good warmups.
Round 1:
mergeCounts.Round 2:
mergeAll.Round 3:
totalWordCount, topWords, and topWord.Round 4:
countDocumentsContaining.0.Round 5:
countWordsExcludingStopWords.Round 6:
countWordsParallel.HashMap.Round 7:
documentLengthsParallel.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.
Try these cases:
thread when documents contain threadstopWords with limit 0, 1, larger than map size, and -1Reset when done:
scripts/reset_exercises.sh lesson_3
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.
By the end of 120 minutes, learners should be able to:
ExecutorService with try-with-resources.Future results and handle interruption correctly.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:
src/demo/java/tutorial/lesson3/demo/ThreadingDemo.javasrc/exercise/java/tutorial/lesson3/exercise/ConcurrentStats.java| Time | Segment | Instructor Moves |
|---|---|---|
| 0-10 | Orientation | Ask learners what concurrency tools they know: async/await, goroutines, channels, promises, worker pools, processes. |
| 10-25 | Run the demo | Run scripts/run.sh lesson_3 demo. Identify sequential, fixed-pool, CompletableFuture, virtual-thread, and atomic-counter output. |
| 25-42 | Fixed thread pool | Open ThreadingDemo.java. Explain submit, Future, get, and executor closure. |
| 42-55 | CompletableFuture | Explain a simple async pipeline and where production code needs error handling. |
| 55-68 | Virtual threads | Explain that virtual threads make blocking task-per-request style cheaper, but do not remove the need for state safety. |
| 68-78 | Shared state | Discuss AtomicInteger and why result aggregation is often clearer than shared mutation. |
| 78-100 | Exercise work | Learners implement the staged concurrency and aggregation lab from workbook.md. |
| 100-112 | Solution compare | Apply answers, run the exercise, and compare error handling choices. |
| 112-118 | Performance discussion | Ask when the parallel solution is slower than sequential. Discuss task overhead and input size. |
| 118-120 | Recap | Summarize: isolate work, return values, merge results, close executors, respect interruption. |
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:
get.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."
Learners edit ConcurrentStats.java.
Recommended implementation path:
countWordsSequential unchanged.mergeCounts and mergeAll first because the parallel solution depends on them.totalWordCount, topWords, and topWord.countWordsParallel by submitting one task per document.InterruptedException, restore the interrupt flag, and throw an unchecked exception.ExecutionException and wrap it.Hints:
documents.size() workers."After applying answers:
scripts/apply_answers.sh lesson_3
scripts/run.sh lesson_3 verify
Expected important lines:
lesson_3 answers verified
HashMap from multiple tasks.InterruptedException.future.get() can throw ExecutionException.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.
private ConcurrentStats() {
public static Map<String, Integer> countWordsSequential(List<String> documents) {
public static Map<String, Integer> countWordsParallel(List<String> documents) {
public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {
public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {
public static int totalWordCount(Map<String, Integer> counts) {
public static Optional<String> topWord(Map<String, Integer> counts) {
public static List<String> topWords(Map<String, Integer> counts, int limit) {
public static int countDocumentsContaining(List<String> documents, String word) {
public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {
public static List<Integer> documentLengthsParallel(List<String> documents) {
private static List<String> wordsIn(String document) {
private ThreadingDemo() {
public static void main(String[] args) throws ExecutionException, InterruptedException {
private static int countPrimesUpTo(int maxInclusive) {
private static boolean isPrime(int value) {
private AnswerVerifier() {
public static void main(String[] args) {
private static void expectEquals(Object expected, Object actual, String label) {
private static void expectThrows(Runnable action, String label) {
private ConcurrentStats() {
public static Map<String, Integer> countWordsSequential(List<String> documents) {
public static Map<String, Integer> countWordsParallel(List<String> documents) {
public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {
public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {
public static int totalWordCount(Map<String, Integer> counts) {
public static Optional<String> topWord(Map<String, Integer> counts) {
public static List<String> topWords(Map<String, Integer> counts, int limit) {
public static int countDocumentsContaining(List<String> documents, String word) {
public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {
public static List<Integer> documentLengthsParallel(List<String> documents) {
private static List<String> wordsIn(String document) {
private ExerciseRunner() {
public static void main(String[] args) {
private ConcurrentStats() {
public static Map<String, Integer> countWordsSequential(List<String> documents) {
public static Map<String, Integer> countWordsParallel(List<String> documents) {
public static Map<String, Integer> mergeCounts(Map<String, Integer> left, Map<String, Integer> right) {
public static Map<String, Integer> mergeAll(List<Map<String, Integer>> maps) {
public static int totalWordCount(Map<String, Integer> counts) {
public static Optional<String> topWord(Map<String, Integer> counts) {
public static List<String> topWords(Map<String, Integer> counts, int limit) {
public static int countDocumentsContaining(List<String> documents, String word) {
public static Map<String, Integer> countWordsExcludingStopWords(List<String> documents, Set<String> stopWords) {
public static List<Integer> documentLengthsParallel(List<String> documents) {
private static List<String> wordsIn(String document) {