In our previous lesson, we learned to identify race conditions, the subtle bugs that arise from the unpredictable timing of concurrent threads accessing shared data. You now know how to spot tell-tale patterns like read-modify-write and check-then-act. However, detection is only the first step. The ultimate goal is to write code that is inherently free from such problems.
This lesson shifts our focus from detection to prevention. We will explore two powerful design patterns that help you build thread-safe applications from the ground up: thread-local storage and immutable data. Instead of adding locks to protect shared data, these techniques eliminate the root cause of the problem. By mastering them, you can create concurrent code that is not only correct but also often simpler to reason about.
The Root of the Problem: Shared Mutable State
Concurrency issues like race conditions almost always boil down to a single cause: multiple threads are accessing shared, mutable state without proper coordination. If the state isn't shared, or if it isn't mutable, the problem vanishes.
This insight gives us a clear strategy for achieving thread safety. The following reading from Carnegie Mellon University's software construction course neatly summarizes the three fundamental ways to fix a program where threads access the same mutable variable.
Concurrency: Safety & Immutability
These lecture slides clearly frame the problem and the high-level solutions.
On slide 12, review the section titled "Synchronization for Safety". It presents three options. We will explore the first two in this lesson. The third option, using synchronization, will be the focus of our next module.
As you just read, our options are:
- Don't share the state variable.
- Make the state variable immutable.
- Use synchronization.
Let's dive into the first two powerful design patterns.
Strategy 1: Don't Share - Thread-Local Storage (TLS)
The most direct way to prevent conflicts over shared data is to simply not share it. Thread-Local Storage (TLS) is a mechanism that achieves this by creating a separate instance of a variable for each thread. A thread-local variable might look like a global or static variable in your code, but behind the scenes, each thread gets its own private copy.
This approach is particularly effective for adapting existing code that wasn't designed for concurrency, especially functions that rely on static variables to maintain state between calls.
The infographic below illustrates this concept perfectly. On the left, multiple threads compete for shared data, leading to race conditions and lock contention. On the right, using Thread-Local Storage gives each thread its own isolated copy, ensuring safety and eliminating contention.

To see this in action, we'll watch a demonstration by Dr. Jacob Sorber. He begins with a classic C function, strtok, which is notoriously not thread-safe because it uses a static variable to remember its position in a string.
This video provides a clear, practical demonstration of a race condition and how TLS elegantly solves it.
First, watch the segment that sets up and demonstrates the problem. Dr. Sorber creates two threads that both try to use his my_str_tok function. Notice how the shared static variable inside the function gets corrupted, leading to incorrect output.
The problem is clear: both threads are overwriting the same previous_state variable. Now, let's see the fix. The magic is a single keyword.
Continuing with the same video, let's observe the solution.
Watch how Dr. Sorber adds the thread_local keyword to the static variable and re-runs the program. The problem is instantly solved. Each thread now has its own previous_state, and they no longer interfere with each other.
This is remarkably powerful. With a minimal code change, we've made a non-thread-safe function safe to use in a concurrent context.
A classic real-world example of TLS is the errno variable in the C standard library. Many system calls report errors by setting this global variable. If errno were truly global, an error in one thread could be misread by another. Modern C libraries solve this by implementing errno as a thread-local variable, ensuring that each thread's error status is kept separate. Dr. Sorber discusses this in the video from
Given your interest in computer architecture, there's another subtle benefit. TLS can improve performance by reducing cache contention. When each thread has its own copy of the data, these copies can be located far apart in memory. This prevents false sharing, a phenomenon where unrelated data items used by different threads happen to fall on the same cache line, causing unnecessary cache invalidations. Dr. Sorber points this out when he shows that the addresses of the thread-local variables are far apart in memory (
Strategy 2: Don't Mutate - Immutable Data
The second strategy is to allow sharing but forbid mutation. An object is immutable if its state cannot be modified after it is created. If data is read-only, any number of threads can access it at the same time without any risk of a data race. There are no writes, so there are no conflicts.
The following video provides a fantastic visual explanation of this idea.
Immutability, visually explained | Code Words
This video uses simple analogies to make the concept of immutability and its benefits very clear.
Watch the introduction, which explains the concept using the analogy of editing a novel. Instead of changing the original, you make a copy. Next, see how this applies to parallel computation. The video shows how shared mutable state leads to a race condition (the counter example). Finally, watch how an immutable approach, where each processor writes its result to a new location, solves the race condition.
Adopting an immutable design philosophy means that instead of changing an object's state, any operation that would "modify" it instead returns a new object with the updated state, leaving the original untouched. This is a core principle of functional programming.
So, how do you create an immutable class? The CMU lecture slides provide a clear, practical checklist.
Concurrency: Safety & Immutability
This reading provides a concrete recipe for designing immutable classes in an object-oriented language like Java, along with important subtleties.
Start at slide 17, titled "Immutability". Read through to slide 32. Pay close attention to: The checklist on slide 20, "Ensuring Immutability", which gives you five concrete rules. The Complex class example, which shows how an add method returns a new Complex object instead of modifying itself. The critical concept of defensive copying on slides 28-29. The Stack example shows how an immutable class can accidentally "leak" a reference to its internal mutable state, and how to fix it. This is a crucial detail for ensuring true immutability.
The key rules for immutability are:
- Make all fields
privateandfinal(orconstin other languages). - Don't provide any methods that modify the object's state (mutators or "setters").
- Ensure that subclasses can't override methods to break immutability (e.g., by making the class
final). - If any fields refer to mutable objects (like a list or another custom class), ensure that neither your constructor nor your accessors share direct references to them. You must create defensive copies on the way in and on the way out.
While immutability offers great benefits in terms of safety and simplicity, it can have performance implications. Creating new objects for every state change can be less efficient than modifying existing ones in place, especially for large objects. This is why you see mutable helper classes like StringBuilder in Java for efficiently building up a complex string. The choice between mutability and immutability is a design trade-off.
Conclusion
In this lesson, you've learned two fundamental design patterns for writing thread-safe code that go beyond simply adding locks. You've seen that the root of many concurrency problems is shared mutable state, and we can address this by attacking either the "shared" part or the "mutable" part.
Here are the key takeaways:
- Thread-Local Storage (TLS) prevents race conditions by eliminating sharing. It gives each thread its own private copy of a variable, which is ideal for managing thread-specific state or adapting legacy code that uses globals.
- Immutability prevents race conditions by eliminating mutation. Immutable objects can be freely shared among threads without any need for synchronization because their state can never change. This leads to code that is often simpler to understand and reason about.
These patterns are your first line of defense against concurrency bugs. By designing for thread safety from the start, you can prevent entire classes of errors from ever occurring.
However, these techniques aren't always applicable. Sometimes, you have a legitimate need for multiple threads to cooperatively modify a single, shared piece of data, such as a task queue or a shared cache. In these cases, we must turn to our third option: explicit synchronization. In the next module, we will begin our deep dive into synchronization primitives, starting with a closer look at the mutex, which you've already seen in action.