Thread Safety and Side Effects
Avoid shared mutable state.
Thread Safety and Side Effects is a free Java Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Parallel Means Concurrent Access
In a parallel stream, multiple threads run your lambdas at the same time. Any shared mutable state they touch becomes a race condition.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int safe = IntStream.rangeClosed(1, 1000).parallel().sum();
System.out.println(safe);
}
}The Danger: Mutating a Shared Variable
A plain int counter touched by many threads loses updates. Never accumulate into shared mutable state inside forEach.
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
AtomicInteger counter = new AtomicInteger();
IntStream.rangeClosed(1, 1000).parallel()
.forEach(n -> counter.incrementAndGet());
System.out.println(counter.get());
}
}Why Plain Collections Break
Adding to a non-thread-safe ArrayList from a parallel forEach can corrupt it or drop elements. The fix is not to share-and-mutate at all.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<Integer> result = IntStream.rangeClosed(1, 10).parallel()
.boxed()
.collect(Collectors.toList());
System.out.println(result.size());
}
}The Right Tool: collect
Use collect instead of side-effecting forEach. The collector framework safely accumulates partial containers per thread and merges them.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
List<Integer> squares = IntStream.rangeClosed(1, 6).parallel()
.map(n -> n * n)
.boxed()
.collect(Collectors.toList());
System.out.println(squares);
}
}The Right Tool: reduce
reduce is inherently thread-safe when the operation is associative and stateless, because it merges partial results without shared mutation.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int sum = IntStream.rangeClosed(1, 1000).parallel()
.reduce(0, Integer::sum);
System.out.println(sum);
}
}Stateless Lambdas
Lambdas in streams should be stateless: their result depends only on the input, not on external mutable variables or earlier elements.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long count = IntStream.rangeClosed(1, 1000).parallel()
.filter(n -> n % 2 == 0)
.count();
System.out.println(count);
}
}Atomic Types as an Escape Hatch
If you truly must share a counter, use atomic classes like AtomicLong. They serialize updates correctly, though they add contention.
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
AtomicLong sum = new AtomicLong();
IntStream.rangeClosed(1, 1000).parallel()
.forEach(sum::addAndGet);
System.out.println(sum.get());
}
}Concurrent Collections
When you need a shared map across threads, a ConcurrentHashMap is safe. Still, prefer Collectors.groupingByConcurrent where possible.
import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("a", "bb", "cc", "ddd");
Map<Integer, List<String>> byLen = words.parallelStream()
.collect(Collectors.groupingByConcurrent(String::length));
System.out.println(byLen);
}
}Avoid Order-Dependent Logic
Do not rely on encounter order inside parallel lambdas. Operations that assume sequential processing produce wrong results in parallel.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int sum = IntStream.rangeClosed(1, 100).parallel()
.map(n -> n * 2)
.sum();
System.out.println(sum);
}
}Pure Functions Parallelize Safely
The golden rule: write pure, stateless, non-interfering operations. Then a stream can be sequential or parallel with identical, correct results.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> in = List.of("red", "green", "blue");
String out = in.parallelStream()
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(","));
System.out.println(out);
}
}Non-Interference
Do not modify the source collection while a stream runs over it. This causes ConcurrentModificationException or undefined behavior, sequential or parallel.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> src = List.of(1, 2, 3, 4);
List<Integer> doubled = src.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(doubled);
}
}Quick Check
What is the recommended way to accumulate results from a parallel stream?
Recap
You kept parallel streams correct:
- Shared mutable state in parallel lambdas causes races.
- Prefer
collectandreduceover side-effectingforEach. - Keep lambdas stateless, pure, and non-interfering.
- If you must share, use atomics or concurrent collections, but that is a last resort.
Frequently asked questions
Is the “Thread Safety and Side Effects” lesson free?
Yes — the full text of “Thread Safety and Side Effects” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.
What will I learn in “Thread Safety and Side Effects”?
Avoid shared mutable state. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Java Academy?
No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Thread Safety and Side Effects” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Java Academy lesson?
Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Creating Parallel Streams
- When Parallelism Helps
- Thread Safety and Side Effects
- Common Pitfalls