Skip to main content
Pixel art: two chefs on opposite sides of a stove both reach for the same frying pan, hands about to collide over the handle. Surprised expressions. On the wall, an unused key hook labeled 'Stove Key' glows teal — the mutex neither chef grabbed. Tagline: Shared State. Shared Problems.

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:

  1. Describe threads as a concurrency mechanism
  2. Recognize the need for synchronization and atomicity
  3. Utilize locks and concurrent collections to implement basic thread-safe code
  4. 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:

UserSceneDevicesSequential 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

LectureTopicThe 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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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
Three-row diagram: row 1 shows concurrency (1 core, interleaved threads), row 2 shows parallelism (3 cores, 3 threads), row 3 shows both (8 cores, 100+ threads interleaved across cores — the real world).

Time-Slicing vs. Parallelism

Two-panel image. The left panel is labelled 'Time-Slicing' and shows a worried chef stirring a pot, with other tasks nearby.
The right panel is labelled 'Parallelism' and shows 3 chefs serenely working on different tasks.

Multiple cores

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

Clip art showing a square microchip labeled 'CPU' with 6 dark rectangles on it, representing coresImage with two rows (like a bar chart), labeled 'Core 1' and 'Core 2'. Different colors in different regions of the rows indicate different tasks.

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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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

StepMain threadcounter1 thread
1new Counter("A", 5)
2counter1.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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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

Diagram showing a CPU containing two cores. Core 0 has Thread-0 and Thread-1, each with its own L1 Cache, feeding into a shared L2 Cache. Core 1 has Thread-2 and Thread-3, each with its own L1 Cache, feeding into a shared L2 Cache. Both cores connect down to Main Memory.

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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

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.");
}
}
StepThread-0Thread-1Plant object
1lastTimeWatered == null? YES
2lastTimeWatered == null? YES
3About to water plant.
4About to water plant.
5Watering plantnumTimesWatered → 1
6Watering plantnumTimesWatered → 2
7numTimesWatered: 2
8numTimesWatered: 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:

  1. Pick up the Husky plush.
  2. Read the whiteboard.
  3. Water the plants if needed.
  4. Update the whiteboard.
  5. 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):

  1. The current thread tries to grab the lock on the object.— If already locked, block the current thread.— Otherwise, grab the lock and proceed.
  2. Hold the lock until the end of the synchronization block.
  3. Release the lock.

This has two benefits:

  1. mutual exclusion: only one thread can be in the block at a time.
  2. 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.

ClassReplacesGuarantee
AtomicIntegerint / IntegerRead-modify-write is atomic (e.g. incrementAndGet())
AtomicReference<T>object referenceAtomic compare-and-swap on any object
ConcurrentHashMap<K,V>HashMapSafe concurrent reads and writes
CopyOnWriteArrayList<T>ArrayListSafe for many readers, few writers
BlockingQueue<T>QueueThread-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:

  1. Code review — look for shared mutable state accessed without synchronization
  2. Static analysis — tools like SpotBugs detect common patterns
  3. Stress testing — activate 10 scenes simultaneously, verify device states
  4. 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

Poll Everywhere QR Code or Logo

Text espertus to 22333 if the
URL isn't working for you.

https://pollev.com/espertus

Key Takeaways

  1. Threads enable concurrency — multiple paths of execution sharing the same heap. Use thread pools, not raw threads.
  2. Shared mutable state is dangerous. Race conditions produce silently wrong results. deviceCount++ is not atomic.
  3. synchronized provides atomicity + visibility. Mutual exclusion prevents interleaving. Memory visibility ensures threads see each other's writes.
  4. Use concurrent collectionsConcurrentHashMap, AtomicInteger, BlockingQueue. The experts already debugged them.
  5. 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
  • CompletableFuture and allOf for fan-out patterns
  • Platform.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): BackgroundTaskRunner wraps 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

TODO