
CS 3100: Program Design and Implementation II
Lecture 31: Concurrency I — Threads and Synchronization
©2026 Jonathan Bell & Ellen Spertus, CC-BY-SA
Learning Objectives
After this lecture, you will be able to:
- Describe threads as a concurrency mechanism
- Recognize the need for synchronization and atomicity
- Utilize locks and concurrent collections to implement basic thread-safe code
- Understand deadlocks and race conditions
50 Devices, 3 Users, 1 Hub
SceneItAll's hub manages 50 smart devices — lights, fans, shades, thermostats — across a whole house.
Three family members walk in and activate scenes simultaneously from different rooms:
| User | Scene | Devices | Sequential time |
|---|---|---|---|
| Alice | "Evening" | 15 devices (dim lights, close shades) | ~5 sec |
| Bob | "Movie Night" | 8 devices (lights off, shades closed) | ~3 sec |
| Carol | "Study" | 6 devices (desk lamp on, overhead dim) | ~2 sec |
Sequential: 10 seconds of delay — Alice waits, Bob waits longer, Carol waits longest.
Concurrent: All three scenes activate within ~5 seconds.
Remember L29: "long handlers freeze the UI." Same problem — now the hub is the UI.
The Concurrency Roadmap: This Week and GA1
| Lecture | Topic | The question it answers |
|---|---|---|
| L31 (Mon) | Threads, shared state, synchronization, deadlock | "Why does my GUI freeze when I load data?" |
| L32 (Wed) | Async programming, CompletableFuture, Platform.runLater() | "How do I load data in the background and update my GUI when it's ready?" |
| L33 (Thu) | Event-driven architecture, consistency, resilience | "What happens when the service I depend on is slow or down?" |
What you'll use in GA1: When CookYourBooks loads recipes from the repository, that's I/O. If you do it on the main thread, your GUI freezes. The GA1 handout provides BackgroundTaskRunner — it wraps threads and FX-thread callbacks into run(callable, onSuccess, onFailure). You don't write threading boilerplate, but you must understand what it does internally (your TA will ask).
Poll: Do multiple apps on your laptop take turns or run simultaneously?
A. they take turns
B. they run at the same time
C. some of both
D. no idea

Text espertus to 22333 if the
URL isn't working for you.
For example, you might have Spotify, VSCode, and Discord all open at once.
Concurrency with and without Parallelism
Concurrency: a program or system with multiple overlapping tasks
- Time-slicing: a single core rapidly switching among tasks
- Parallelism: multiple cores simultaneously executing different tasks

Time-Slicing vs. Parallelism

Multiple cores
Modern processors have multiple "cores" (mini-processors), each of which can run a different task, giving true parallelism.


Both parallelism and time-slicing are used.
Poll: Parallelism vs. Time-Slicing
Do you use parallelism or time-slicing for taking multiple courses in a semester?
A. parallelism
B. time-slicing
C. both
D. neither
E. not sure

Text espertus to 22333 if the
URL isn't working for you.
A Thread Is an Independent Path of Execution
Every Java program has a main thread. You can create additional threads that run concurrently. Threads share the code and heap (data memory) but have their own stack (call history) and instruction pointers.
Poll: Do Java threads run in parallel (at the same time)?
If there are ten concurrent threads, do they run in parallel (on 10 different cores) or using time-slicing on a single core?
A. they run on 10 different cores
B. they run on a single core
C. it depends

Text espertus to 22333 if the
URL isn't working for you.
Counter Example A
public class Counter extends Thread {
private final String name;
private int count;
private final int limit;
public Counter(String name, int limit) {
this.name = name;
count = 0;
this.limit = limit;
}
@Override
public void run() {
while (count < limit) {
count++;
System.out.println(name + ": " + count);
}
}
public static void main(String[] args) {
Counter counter1 = new Counter("Counter A", 5);
counter1.start();
}
}
Counter A: 1 Counter A: 2 Counter A: 3 Counter A: 4 Counter A: 5
| Step | Main thread | counter1 thread |
|---|---|---|
| 1 | new Counter("A", 5) | |
| 2 | counter1.start() → | spawned |
| 3 | (continues) | run() — counts 1→5 |
Counter Example B
public class Counter extends Thread {
private final String name;
private int count;
private final int limit;
public Counter(String name, int limit) { ... }
private static void printThread(String msg) {
System.out.println(String.format("thread %s: %s",
Thread.currentThread().getName(), msg));
}
@Override
public void run() {
while (count < limit) {
count++;
printThread(name + ": " + count);
}
}
public static void main(String[] args) {
printThread("At start of main()");
Counter counter1 = new Counter("Counter A", 5);
counter1.start();
}
}
thread main: At start of main()
thread Thread-0: Counter A: 1
thread Thread-0: Counter A: 2
thread Thread-0: Counter A: 3
thread Thread-0: Counter A: 4
thread Thread-0: Counter A: 5
Poll: What could be the last line printed?
@Override
public void run() {
while (count < limit) {
count++;
printThread(name + ": " + count);
}
}
public static void main(String[] args) {
printThread("At start of main()");
Counter counter1 = new Counter("Counter A", 5);
counter1.start();
printThread("At end of main()");
}
A. thread main: At end of main()
B. thread Thread-0: At end of main()
C. thread Thread-0: Counter A: 4
D. thread Thread-0: Counter A: 5
E. I have no idea

Text espertus to 22333 if the
URL isn't working for you.
Counter Example C
public class Counter extends Thread {
private final String name;
private int count;
private final int limit;
public Counter(String name, int limit) { ... }
private static void printThread(String msg) {
System.out.println(String.format("thread %s: %s",
Thread.currentThread().getName(), msg));
}
@Override
public void run() {
while (count < limit) {
count++;
printThread(name + ": " + count);
}
}
public static void main(String[] args) {
printThread("At start of main()");
Counter counter1 = new Counter("Counter A", 3);
counter1.start();
printThread("At end of main()");
}
}
thread main: At start of main()
thread main: At end of main()
thread Thread-0: Counter A: 1
thread Thread-0: Counter A: 2
thread Thread-0: Counter A: 3
thread main: At start of main()
thread Thread-0: Counter A: 1
thread main: At end of main()
thread Thread-0: Counter A: 2
thread Thread-0: Counter A: 3
thread main: At start of main()
thread Thread-0: Counter A: 1
thread Thread-0: Counter A: 2
thread main: At end of main()
thread Thread-0: Counter A: 3
thread main: At start of main()
thread Thread-0: Counter A: 1
thread Thread-0: Counter A: 2
thread Thread-0: Counter A: 3
thread main: At end of main()
The output is nondeterministic.
The Big Picture
-
Java programs can have multiple threads.
-
Code in different threads can run concurrently, which can be
- in parallel (simultaneously) on different cores
- using time-slicing (interleaving) on a single core
-
This can lead to nondeterministic (unpredictable) behavior.
-
Why is nondeterminism a problem?
Complex Number Example A
public class ComplexNumber {
private int real;
private int imaginary;
public ComplexNumber(int real, int imaginary) {
this.real = real;
this.imaginary = imaginary;
}
public void negate() {
printThread("At start of negate()");
real = -real;
imaginary = -imaginary;
printThread("At end of negate()");
}
@Override
public String toString() {
return String.format("%d + %di", real, imaginary);
}
public static void main(String[] args) {
ComplexNumber c = new ComplexNumber(1, 1);
printThread("Original: " + c);
c.negate();
printThread("Negated: " + c);
}
}
thread main: Original: 1 + 1i thread main: At start of negate() thread main: At end of negate() thread main: Negated: -1 + -1i
Complex Number Example B
public class ComplexNumber {
private int real;
private int imaginary;
public ComplexNumber(int real, int imaginary) { .. }
public void negate() {
printThread("At start of negate()");
real = -real;
imaginary = -imaginary;
printThread("At end of negate()");
}
@Override
public String toString() {
return String.format("%d + %di", real, imaginary);
}
public static void main(String[] args) {
ComplexNumber c = new ComplexNumber(1, 1);
printThread("Original: " + c);
new Thread(c::negate).start(); // easy way to create thread
printThread("Negated: " + c);
}
}
Poll: What can be printed at the end of main() for the negated value?
Poll: What can be printed for the negated value?
public static void main(String[] args) {
ComplexNumber c = new ComplexNumber(1, 1);
printThread("Original: " + c);
new Thread(c::negate).start();
printThread("Negated: " + c);
}
A. 1 + 1i
B. -1 + 1i
C. 1 + -1i
D. -1 + -1i
E. no idea

Text espertus to 22333 if the
URL isn't working for you.
Possible Outputs: Complex Number Example B
thread main: Original: 1 + 1i
thread main: Negated: 1 + 1i
thread Thread-0: At start of negate()
thread Thread-0: At end of negate()
thread main: Original: 1 + 1i
thread Thread-0: At start of negate()
thread main: Negated: 1 + 1i
thread Thread-0: At end of negate()
thread main: Original: 1 + 1i
thread Thread-0: At start of negate()
thread Thread-0: At end of negate()
thread main: Negated: -1 + -1i
thread main: Original: 1 + 1i
thread Thread-0: At start of negate()
thread main: Negated: -1 + 1i
thread Thread-0: At end of negate()
This program is not only nondeterministic but has a race condition:
We can get a wrong result due to timing.
There's an additional problem I'm not revealing yet.
Problem 1: Order of Execution
public static void main(String[] args) {
ComplexNumber c = new ComplexNumber(1, 1);
printThread("Original: " + c);
new Thread(c::negate).start();
printThread("Negated: " + c);
}
No guarantee Thread-0 runs before main prints "Negated".
Problem 2: Cached Values
public static void main(String[] args) {
ComplexNumber c = new ComplexNumber(1, 1);
printThread("Original: " + c);
new Thread(c::negate).start();
printThread("Negated: " + c);
}
Even if Thread-0 completes before main resumes, main's cache may hold stale values.
Java Memory Model

Hard Things in Computer Science
"There are only two hard things in Computer Science: cache invalidation and naming things" — Phil Karlton
"There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors." — Jeff Atwood
Two Users Activate Different Scenes at the Same Time
Alice activates "Party" (lights to 10%, music loud). Bob activates "Study" (lights to 100%, music off). Same room, same instant. This code handles it:
public class SceneService {
public boolean activateScene(Scene scene, Area room) {
for (Device device : room.getDevices()) {
DeviceState targetState = scene.getTargetState(device);
if (targetState != null) {
device.setState(targetState);
}
}
return true;
}
}
Looks correct. What could go wrong?
Poll: What should happen when users make conflicting requests?
What if these requests are made at the same time by different people:
- Activate Party scene (lights to 10%, music loud)
- Activate Study scene (lights to 100%, music off) What should be permissible outcomes?
A. The slightly earlier scene is activated
B. The slightly later scene is activated
C. Either of the above
D. A mixture occurs
E. No changes are made

Text espertus to 22333 if the
URL isn't working for you.
The Interleaving (1/3): Alice Goes First
Alice sets the lights to 10%. So far so good...
The Interleaving (2/3): Bob Overwrites Alice
Bob overwrites Alice's light setting. The lights are now 100% — Study mode wins on lights.
The Interleaving (3/3): Nobody Got What They Wanted
Both calls to activateScene() returned true, but neither scene was correctly applied.
Our code has a race condition.
You Can't Even Trust deviceCount++ (1/2)
deviceCount++ is three operations: read, add, write. Two threads registering devices simultaneously:
Both threads read 0. Both compute 1. What happens when they write?
You Can't Even Trust deviceCount++ (2/2)
Lost update: Thread A writes 1. Thread B writes 1 — overwrites A's result with the same stale computation. One increment is silently lost. If you can't trust deviceCount++, how do you trust anything?
Plant Example A
public class Plant implements Runnable {
private static final int WATERING_INTERVAL_DAYS = 3;
private Instant lastTimeWatered;
private int numTimesWatered = 0;
private int daysSinceWatered() {
return (int) Duration.between(lastTimeWatered,
Instant.now()).toDays();
}
private void waterPlant() {
printThread("Watering plant.");
numTimesWatered++;
lastTimeWatered = Instant.now();
}
@Override
public void run() {
if (lastTimeWatered == null
|| daysSinceWatered() >= WATERING_INTERVAL_DAYS) {
printThread("About to water plant.");
waterPlant();
printThread("numTimesWatered: " + numTimesWatered);
} else {
printThread("No need to water plant.");
}
}
public static void main(String[] args) {
Plant plant = new Plant();
new Thread(plant).start(); // roommate 1
new Thread(plant).start(); // roommate 2
}
}
thread Thread-0: About to water plant.
thread Thread-1: About to water plant.
thread Thread-0: Watering plant
thread Thread-1: Watering plant
thread Thread-1: numTimesWatered: 2
thread Thread-0: numTimesWatered: 2
Race Condition: Plant Watering
private void waterPlant() {
printThread("Watering plant.");
numTimesWatered++;
lastTimeWatered = Instant.now();
}
@Override
public void run() {
if (lastTimeWatered == null
|| daysSinceWatered() >= WATERING_INTERVAL_DAYS) {
printThread("About to water plant.");
waterPlant();
printThread("numTimesWatered: " + numTimesWatered);
} else {
printThread("No need to water plant.");
}
}
| Step | Thread-0 | Thread-1 | Plant object |
|---|---|---|---|
| 1 | lastTimeWatered == null? YES | ||
| 2 | lastTimeWatered == null? YES | ||
| 3 | About to water plant. | ||
| 4 | About to water plant. | ||
| 5 | Watering plant | numTimesWatered → 1 | |
| 6 | Watering plant | numTimesWatered → 2 | |
| 7 | numTimesWatered: 2 | ||
| 8 | numTimesWatered: 2 |
How to Solve Plant Watering?
Two roommates have a whiteboard that shows when the plants were last watered.
At any time, a roommate can check if it's been more than X days since the plants were watered. If so,
- water the plant
- update the date on the whiteboard
This can lead to the plant getting watered twice on the same day.
How can this problem be fixed without direct communication? (Assume they're both invisible.)
If it helps, you can assume there is a Husky plush.
Synchronizing on a Shared Object
Have a rule that a roommate must follow this sequence:
- Pick up the Husky plush.
- Read the whiteboard.
- Water the plants if needed.
- Update the whiteboard.
- Put down the Husky plush.
Steps 2-4 cannot be done unless holding the Husky plush.
Plant Example B
public class Plant implements Runnable {
private static final int WATERING_INTERVAL_DAYS = 3;
private Instant lastTimeWatered;
private int numTimesWatered = 0;
private final Object huskyPlush = new Object(); // NEW
private int daysSinceWatered() { .. }
private void waterPlant() {
printThread("Watering plant.");
numTimesWatered++;
lastTimeWatered = Instant.now();
}
@Override
public void run() {
synchronized (huskyPlush) { // NEW
if (lastTimeWatered == null
|| daysSinceWatered() >= WATERING_INTERVAL_DAYS) {
printThread("About to water plant.");
waterPlant();
printThread("numTimesWatered: " + numTimesWatered);
} else {
printThread("No need to water plant.");
}
}
}
public static void main(String[] args) {
Plant plant = new Plant();
new Thread(plant).start(); // roommate 1
new Thread(plant).start(); // roommate 2
}
}
My output:
thread Thread-0: About to water plant.
thread Thread-0: Watering plant...
thread Thread-0: numTimesWatered: 1
thread Thread-1: Plant does not need watering.
The only other possible output:
thread Thread-1: About to water plant.
thread Thread-1: Watering plant...
thread Thread-1: numTimesWatered: 1
thread Thread-0: Plant does not need watering.
Synchronization
Synchronizing on an object (such as huskyPlush means):
- The current thread tries to grab the lock on the object.— If already locked, block the current thread.— Otherwise, grab the lock and proceed.
- Hold the lock until the end of the synchronization block.
- Release the lock.
This has two benefits:
- mutual exclusion: only one thread can be in the block at a time.
- visibility: writes made by one thread within the block are visible to the next thread that acquires the lock
Synchronization objects
We currently have this:
@Override
public void run() {
synchronized (huskyPlush) { ... }
}
}
Instead of creating a new object, we could synchronization on the instance
(invoking object), the Plant we are calling run() on:
@Override
public void run() {
synchronized (this) { ... }
}
This can be stated more concisely:
@Override
public void synchronized run() {
...
}
Synchronization is hard
-
Three broken examples
-
Data structures that can help
What's Wrong With Plant Example D?
public class Plant implements Runnable {
// unchanged code omitted
private void waterPlant() {
printThread("Watering plant.");
numTimesWatered++;
lastTimeWatered = Instant.now();
}
@Override
public synchronized void run() {
if (lastTimeWatered == null
|| daysSinceWatered() >= WATERING_INTERVAL_DAYS) {
printThread("About to water plant.");
waterPlant();
printThread("numTimesWatered: " + numTimesWatered);
} else {
printThread("No need to water plant.");
}
}
public static void main(String[] args) {
Plant plant = new Plant();
new Thread(plant).run(); // roommate 1
new Thread(plant).run(); // roommate 2
}
}
thread main: About to water plant.
thread main: Watering plant.
thread main: numTimesWatered: 1
thread main: No need to water plant.
What's Wrong With Plant Example E?
public class Plant implements Runnable {
// unchanged code omitted
private void waterPlantIfNeeded() {
if (lastTimeWatered == null
|| daysSinceWatered() >= WATERING_INTERVAL_DAYS) {
printThread("About to water plant.");
waterPlant();
printThread("numTimesWatered: " + numTimesWatered);
} else {
printThread("No need to water plant.");
}
}
@Override
public synchronized void run() {
waterPlantIfNeeded();
}
public static void main(String[] args) {
Plant plant = new Plant();
new Thread(plant).start(); // roommate 1
new Thread(plant::waterPlantIfNeeded) // roommate 2
.start();
}
}
thread Thread-0: About to water plant.
thread Thread-1: About to water plant.
thread Thread-0: Watering plant.
thread Thread-1: Watering plant.
thread Thread-0: numTimesWatered: 2
thread Thread-1: numTimesWatered: 2
The lock is useless if threads can enter the critical section another way.
What's Wrong With Dining Example A?
public class DiningExample {
private final Object fork = new Object();
private final Object knife = new Object();
public void roommate1() {
synchronized (fork) {
printThread("Roommate 1 picked up fork.");
synchronized (knife) {
printThread("Roommate 1 picked up knife. Eating!");
}
}
}
public void roommate2() {
synchronized (knife) {
printThread("Roommate 2 picked up knife.");
synchronized (fork) {
printThread("Roommate 2 picked up fork. Eating!");
}
}
}
public static void main(String[] args) {
DiningExample d = new DiningExample();
new Thread(d::roommate1).start();
new Thread(d::roommate2).start();
}
}
thread Thread-0: Roommate 1 picked up fork.
thread Thread-1: Roommate 2 picked up knife.
This is a deadlock: two threads each waiting for a lock held by the other.
Concurrent Data Structures
Java provides thread-safe data structures in java.util.concurrent that handle synchronization internally — no synchronized blocks needed.
| Class | Replaces | Guarantee |
|---|---|---|
AtomicInteger | int / Integer | Read-modify-write is atomic (e.g. incrementAndGet()) |
AtomicReference<T> | object reference | Atomic compare-and-swap on any object |
ConcurrentHashMap<K,V> | HashMap | Safe concurrent reads and writes |
CopyOnWriteArrayList<T> | ArrayList | Safe for many readers, few writers |
BlockingQueue<T> | Queue | Thread-safe producer-consumer handoff |
Prefer these over manual synchronized blocks — they are faster, less error-prone, and express intent clearly.
Concurrency Bugs Are the Hardest Bugs
Why they're hard:
- Timing-dependent — the bug only appears under specific interleavings
- Non-reproducible — run the same test twice, get different results
- Load-dependent — works fine with 5 devices, glitches with 50
The family's lights work fine with 5 devices. Add a 6th and scenes start glitching. Add a firmware update and the hub deadlocks. These bugs grow with scale.
Detection strategies:
- Code review — look for shared mutable state accessed without synchronization
- Static analysis — tools like SpotBugs detect common patterns
- Stress testing — activate 10 scenes simultaneously, verify device states
- Design — prefer immutable objects, concurrent collections, minimal shared state
"The best concurrency bug is the one you avoid by not sharing mutable state."
Poll: Share Something You Learned Today

Text espertus to 22333 if the
URL isn't working for you.
Key Takeaways
- Threads enable concurrency — multiple paths of execution sharing the same heap. Use thread pools, not raw threads.
- Shared mutable state is dangerous. Race conditions produce silently wrong results.
deviceCount++is not atomic. synchronizedprovides atomicity + visibility. Mutual exclusion prevents interleaving. Memory visibility ensures threads see each other's writes.- Use concurrent collections —
ConcurrentHashMap,AtomicInteger,BlockingQueue. The experts already debugged them. - Prevent deadlock with consistent lock ordering. Always acquire locks in the same order across all code paths.
Looking Ahead
L32 (Wednesday): Asynchronous Programming
- Activating "Evening" means sending commands to 15 devices — each one a 200ms network call
- Threads are expensive for all that waiting
CompletableFutureandallOffor fan-out patternsPlatform.runLater()for updating your GA1 GUI from a background thread
Lab 12 runs Monday — GUI pair programming with Scene Builder
Your group project:
- GA1 (due Apr 9):
BackgroundTaskRunnerwraps the threading concepts from today - Your TA will ask what happens under the hood — be ready to explain threads, shared state, and synchronization
Today: why shared state is dangerous and how locks protect it. Wednesday: how to avoid blocking threads entirely.
Bonus Slide
