Skip to main content
Pixel art: a character reads a book at a cafe. Their phone buzzes on the table with a notification showing a light bulb and green checkmark. Through the window, smart home devices work on their own. Tagline: Don't Wait. Get Called Back.

CS 3100: Program Design and Implementation II

Lecture 32: Concurrency II — Asynchronous Programming

©2026 Ellen Spertus & Jonathan Bell, CC-BY-SA

Learning Objectives

After this lecture, you will be able to:

  1. Distinguish whether an operation should run on the GUI thread or a background thread
  2. Explain what problems asynchronous programming solves
  3. Use BackgroundTaskRunner to divide work between GUI and background threads
  4. Identify and avoid common asynchronous programming pitfalls

Big Picture

GUI apps need to stay responsive while doing slow work. The naive fix — spawning a thread — works but has costs.

Today you'll learn a cleaner approach you'll use directly in Lab 13 and GA1.

Cook My Books Example A

public class CookMyBooks {
private static final long START_TIME = System.currentTimeMillis();

private static void log(String message) {
long elapsed = System.currentTimeMillis() - START_TIME;
String thread = Thread.currentThread().getName();
System.out.printf("%4dms %s %s%n", elapsed, thread, message);
}

private static String fetchRecipe() {
log("Fetching recipe...");
try {
Thread.sleep(2000); // simulates slow network call
} catch (InterruptedException e) {
// this can't happen in our program
}
log("Got recipe!");
return "Cookie Recipe";
}

private static void updateUI(String recipe) {
log("Updating UI: " + recipe);
}

// This runs in the UI thread.
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
log("UI is non-responsive");
String recipe = fetchRecipe();
updateUI(recipe);
log("UI is now responsive again");
}

public static void main(String[] args) {
handleFetchRecipeRequest();
}
}
    0ms main UI thread received fetch recipe request
20ms main UI is non-responsive
21ms main Fetching recipe...
2036ms main Got recipe!
2038ms main Updating UI: Cookie Recipe
2038ms main UI is now responsive again

Cook My Books Example B

public class CookMyBooks {
private static final long START_TIME = System.currentTimeMillis();

private static void log(String message) { .. }

private static String fetchRecipe() {
log("Fetching recipe...");
try {
Thread.sleep(2000); // simulates slow network call
} catch (InterruptedException e) { .. }
log("Got recipe!");
return "Cookie Recipe";
}

private static void updateUI(String recipe) {
log("Updating UI: " + recipe);
}

// This runs in the UI thread
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
log("UI is non-responsive");

// Create a worker thread to fetch the recipe
new Thread(CookMyBooks::fetchRecipe).start();
log("UI is now responsive again");
// How do we update the UI with the recipe?
}

public static void main(String[] args) {
handleFetchRecipeRequest();
}
}
    0ms main UI thread received fetch recipe request
23ms main UI is non-responsive
24ms main UI is now responsive again
24ms Thread-0 Fetching recipe...
2035ms Thread-0 Got recipe!

✅ UI remains responsive

❌ How does UI get updated with recipe?

Cook My Books Example C

public class CookMyBooks {
// ... log() unchanged

private static void fetchRecipe() {
log("Fetching recipe...");
try {
Thread.sleep(2000); // simulates slow network call
} catch (InterruptedException e) {
// this can't happen in our program
}
log("Got recipe!");
updateUI("Cookie Recipe");
}

private static void updateUI(String recipe) {
if (!Thread.currentThread().getName().equals("main")) {
throw new IllegalStateException(
"updateUI must be called on the main thread");
}
log("Updating UI: " + recipe);
}

// This runs in the UI thread.
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
log("UI is non-responsive");
new Thread(CookMyBooks::fetchRecipe).start();
log("UI is now responsive again");
}

public static void main(String[] args) {
handleFetchRecipeRequest();
}
}
    0ms main UI thread received fetch recipe request
16ms main UI is non-responsive
19ms main UI is now responsive again
19ms Thread-0 Fetching recipe...
2029ms Thread-0 Got recipe!
Exception in thread "Thread-0" java.lang.IllegalStateException:
updateUI must be called on the main thread

Asynchronous Programming

Asynchronous: start an operation and move on; get notified when it completes.

Original single-threaded synchronous version

Asynchronous Programming: The Pattern

To run a task asynchronously, you need to specify:

  1. What task to run in the background
  2. What to do on success
  3. What to do on failure
Java (GA1)Java (general)JavaScriptPython
MechanismBackgroundTaskRunnerCompletableFuturePromise / async/awaitasyncio / concurrent.futures
Background taskcallablesupplyAsync()async functionasync def / submit()
On successonSuccessthenAccept().then()await / add_done_callback()
On failureonFailureexceptionally().catch()try/except / add_done_callback()

BackgroundTaskRunner Signature

public static <T> Task<T> run(
Callable<T> callable, // () -> fetchRecipe()
Consumer<T> onSuccess, // (String recipe) -> updateUI(recipe)
Consumer<Throwable> onFailure // (Throwable error) -> showError(error)
)

In our example, the type argument T is String.

TypeMeaningExample
Callable<T>A lambda that takes no arguments and returns a value of type T() -> fetchRecipe() returns String
Consumer<T>A lambda that takes a value of type T and returns nothing(String recipe) -> updateUI(recipe)
Consumer<Throwable>A lambda that takes an exception and returns nothing(Throwable error) -> showError(error)

Cook My Books Example D

public class CookMyBooks {
// ... log(), fetchRecipe(), updateUI(), showError() unchanged

// This runs in the UI thread.
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
BackgroundTaskRunner.run(
() -> fetchRecipe(),
recipe -> updateUI(recipe),
error -> showError(error));
log("UI is now responsive again");
}

public static void main(String[] args) {
Platform.startup(() -> {}); // initialize JavaFX without a GUI
Platform.runLater(() -> handleFetchRecipeRequest());
}
}
 419ms JavaFX Application Thread UI thread received fetch recipe request
445ms JavaFX Application Thread UI is now responsive again
446ms Thread-2 Fetching recipe...
2447ms Thread-2 Got recipe!
2448ms JavaFX Application Thread Updating UI: Cookie Recipe

✅ UI remains responsive

✅ UI gets updated with recipe on FX thread

The Task Object

BackgroundTaskRunner.run() returns a Task<T> object that can be used to monitor or cancel the background operation:

Task<String> task = BackgroundTaskRunner.<String>run(
() -> fetchRecipe(),
recipe -> updateUI(recipe),
error -> showError(error));
MethodPurpose
task.cancel()Request cancellation of the task
task.isCancelled()Check if the task has been cancelled — useful inside the callable to stop early
task.isDone()Checks if the task has completed (succeeded or failed)

Cook My Books Example E

public class CookMyBooks {
// ... log(), updateUI(), showError() unchanged
private static Task<String> fetchRecipeTask = null;

private static String fetchRecipe() {
log("Fetching recipe...");
try {
Thread.sleep(2000); // simulates slow network call
} catch (InterruptedException e) {
log("fetchRecipe() was interrupted");
log("isCancelled is: " + fetchRecipeTask.isCancelled());
return null;
}
log("Got recipe!");
return "Cookie Recipe";
}

// This runs in the UI thread
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
fetchRecipeTask = BackgroundTaskRunner.run(
() -> fetchRecipe(),
recipe -> updateUI(recipe),
error -> showError(error));
log("UI is now responsive again");
}

// This runs in the UI thread
private static void handleCancelFetchRecipeRequest() {
log("UI thread received cancel request");
if (fetchRecipeTask != null) {
fetchRecipeTask.cancel();
}
}

public static void main(String[] args) throws InterruptedException {
Platform.startup(() -> {}); // initialize JavaFX without a GUI
Platform.runLater(() -> handleFetchRecipeRequest());
Thread.sleep(1000); // simulate cancel after 1 second
handleCancelFetchRecipeRequest();
}
}
 434ms JavaFX Application Thread UI thread received fetch recipe request
459ms JavaFX Application Thread UI is now responsive again
459ms Thread-2 Fetching recipe...
1433ms main UI thread received cancel request
1434ms Thread-2 fetchRecipe() was interrupted
1437ms Thread-2 isCancelled is: true

✅ Task is cancelled after 1 second

✅ Worker thread is interrupted immediately

isCancelled() returns true inside the callable

What Goes on Which Thread?

FX / UI thread:

  • Updating labels, lists, buttons
  • Responding to user input
  • Anything touching ObservableList or Property
  • Short, fast operations

Background / worker thread:

  • Reading or writing files
  • Network calls
  • Database queries
  • Heavy computation
  • Anything slow

Why? I/O operations can take thousands of times longer than UI operations. Blocking the FX thread on I/O freezes the entire UI — no redraws, no input handling, nothing.

I/O Takes an Eternity (from the CPU's Perspective)

Bar chart showing operation times scaled so 1 CPU cycle = 1 second. RAM access = 5 minutes. Zigbee device round trip = 4 months. Cross-country API call = 5 years.

Poll: Which should run on a background thread?

A. Loading a list of collections from the database

B. Updating a ListView with search results

C. Computing optimal scene settings from sensor data

D. Handling a button click

E. Mining bitcoin

Poll Everywhere QR Code or Logo

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

https://pollev.com/espertus

Async Pitfalls

  1. Running scheduled code on the wrong thread
  2. Cancelling tasks running on worker threads
  3. Preventing race conditions

You are required to handle all of these correctly in GA1.
Let's find out how.

Introducing Pitfall 1: Timer Thread Mistakes

Pixel art illustration divided into two panels. Left panel labeled 'Library View: Undo Delete': a cookbook app screen showing a list of collections with 'Italian Classics' greyed out and crossed through. A teal notification bar reads 'Collection deleted.' with an Undo button and a circular countdown timer showing 4 seconds remaining. Right panel labeled 'Search & Filter: Debouncing': a cookbook app search screen with 'choc' typed in the search bar and a loading spinner. Results show three recipe cards: Chocolate Cake, Chocolate Chip Cookies, and Hot Chocolate, updating in real time as the user types.

Pitfall 1: Running Scheduled Code on the Wrong Thread

In CookYourBooks, two features require running code after a delay:

Library View: After the user deletes a collection, show an "Undo" button for 5 seconds. If the timer expires without the user clicking Undo, permanently delete the collection.

Search & Filter: Don't fire a search on every keystroke. Wait 300ms after the user stops typing, then run the search.

PauseTransition

Waits for a specified duration, then fires a callback on the FX thread:

PauseTransition timer = new PauseTransition(Duration.seconds(5));
timer.setOnFinished(e -> doSomething()); // runs on FX thread
timer.play(); // start the timer
timer.stop(); // cancel the timer (e.g., on each keystroke)
MethodPurpose
new PauseTransition(Duration)Create a timer with a specified duration
setOnFinished(handler)Set the callback to run when the timer expires
play()Start (or restart) the timer
stop()Cancel the timer

Because PauseTransition is part of the JavaFX animation system, its onFinished handler fires on the FX Application Thread automatically.

Pitfall 2: Task Cancellation

Import Interface: The user selects an image and clicks Import. OCR runs on a background thread. While it's running, a Cancel button is displayed. If the user clicks Cancel, the import should stop and the UI should return to the idle state.

Task.cancel() does two things:

  • Interrupts the background thread if it is blocked
  • Sets isCancelled() to true

But it does not trigger onSuccess or onFailure. If you rely on onFailure to transition your UI back to idle, nothing will happen.

Fix: Handle the state transition directly in your cancel method — do not rely on the callbacks.

Cook My Books Example F

public class CookMyBooks {
// ... log(), fetchRecipe(), updateUI(), showError() unchanged
private static Task<String> fetchRecipeTask = null;

// This runs in the UI thread
private static void handleFetchRecipeRequest() {
log("UI thread received fetch recipe request");
fetchRecipeTask = BackgroundTaskRunner.run(
() -> fetchRecipe(),
recipe -> updateUI(recipe),
error -> showError(error));
log("UI is now responsive again");
}

// This runs in the UI thread
private static void handleCancelFetchRecipeRequest() {
log("UI thread received cancel request");
if (fetchRecipeTask != null) {
fetchRecipeTask.cancel();
updateUI("fetch recipe request cancelled");
fetchRecipeTask = null;
}
}

public static void main(String[] args) throws InterruptedException {
Platform.startup(() -> {}); // initialize JavaFX without a GUI
Platform.runLater(() -> handleFetchRecipeRequest());
Thread.sleep(1000); // simulate cancel after 1 second
Platform.runLater(() -> handleCancelFetchRecipeRequest());
}
}
 445ms JavaFX Application Thread UI thread received fetch recipe request
469ms JavaFX Application Thread UI is now responsive again
469ms Thread-2 Fetching recipe...
1444ms JavaFX Application Thread UI thread received cancel request
1448ms Thread-2 fetchRecipe() was interrupted
1448ms JavaFX Application Thread Updating UI: fetch recipe request cancelled
1449ms Thread-2 isCancelled is: true

✅ Both handlers run on the FX Application Thread

✅ Task is cancelled after 1 second

✅ UI is updated directly in the cancel handler — not via callback

Cook My Books Example G

public class CookMyBooks {
// ... log(), updateUI(), showError() unchanged
private static Task<String> calculatePiTask = null;

private static String calculatePi() {
double pi = 0;
for (int i = 0; i < 100_000_000; i++) {
pi += Math.pow(-1, i) / (2 * i + 1);
}
pi *= 4;
log("Calculated Pi: " + pi);
return Double.toString(pi);
}

// This runs in the UI thread
private static void handleCalculatePiRequest() {
log("UI thread received calculate Pi request");
calculatePiTask = BackgroundTaskRunner.run(
() -> calculatePi(),
pi -> updateUI(pi),
error -> showError(error));
log("UI is now responsive again");
}

// This runs in the UI thread
private static void handleCancelCalculatePiRequest() {
log("UI thread received cancel request");
if (calculatePiTask != null) {
calculatePiTask.cancel();
updateUI("calculate Pi request cancelled");
calculatePiTask = null;
}
}

public static void main(String[] args) throws InterruptedException {
Platform.startup(() -> {}); // initialize JavaFX without a GUI
Platform.runLater(() -> handleCalculatePiRequest());
Thread.sleep(1000); // simulate cancel after 1 second
Platform.runLater(() -> handleCancelCalculatePiRequest());
}
}
 449ms JavaFX Application Thread UI thread received calculate Pi request
474ms JavaFX Application Thread UI is now responsive again
1448ms JavaFX Application Thread UI thread received cancel request
1454ms JavaFX Application Thread Updating UI: calculate Pi request cancelled
3215ms Thread-2 Calculated Pi: 3.141592643589326

✅ UI is updated directly in the cancel handler

cancel() does not interrupt a CPU-bound loop

❌ Result is computed but never shown — callback never fires

Cook My Books Example H

public class CookMyBooks {
// ... log(), updateUI(), showError() unchanged
private static volatile Task<String> calculatePiTask = null;

private static String calculatePi() {
double pi = 0;
for (int i = 0; i < 100_000_000; i++) {
pi += Math.pow(-1, i) / (2 * i + 1);
if (calculatePiTask != null && calculatePiTask.isCancelled()) {
log("calculatePi was cancelled after " + i + " iterations");
return "Cancelled";
}
}
pi *= 4;
log("Calculated Pi: " + pi);
return Double.toString(pi);
}
}
 413ms JavaFX Application Thread UI thread received calculate Pi request
435ms JavaFX Application Thread UI is now responsive again
1412ms JavaFX Application Thread UI thread received cancel request
1414ms JavaFX Application Thread Updating UI: calculate Pi request cancelled
1414ms Thread-2 calculatePi was cancelled after 36863664 iterations
  // This runs in the UI thread
private static void handleCalculatePiRequest() {
log("UI thread received calculate Pi request");
calculatePiTask = BackgroundTaskRunner.run(
() -> calculatePi(),
pi -> updateUI(pi),
error -> showError(error));
log("UI is now responsive again");
}

// This runs in the UI thread
private static void handleCancelCalculatePiRequest() {
log("UI thread received cancel request");
if (calculatePiTask != null) {
calculatePiTask.cancel();
updateUI("calculate Pi request cancelled");
calculatePiTask = null;
}
}

✅ CPU-bound loop stops early by checking isCancelled()

✅ UI updated directly in cancel handler

✅ Neither callback fires

The volatile Keyword

Recall from L31: threads can cache variable values locally, leading to visibility problems.

// Without volatile: background thread may never see the update
private static Task<String> calculatePiTask = null;

// With volatile: all threads always see the most recent value
private static volatile Task<String> calculatePiTask = null;

Use volatile when:

  • A variable is written by one thread and read by another
  • You only need visibility, not atomicity

volatile is not enough when you need atomicity:

  • Use AtomicInteger for count++
  • CookMyBooks Example H

Cook My Books Example H Revisited

public class CookMyBooks {
// ... log(), updateUI(), showError() unchanged
private static volatile Task<String> calculatePiTask = null;

private static String calculatePi() {
double pi = 0;
for (int i = 0; i < 100_000_000; i++) {
pi += Math.pow(-1, i) / (2 * i + 1);
if (calculatePiTask != null &&
// What if calculatePiTask becomes null?
calculatePiTask.isCancelled()) {
log("calculatePi was cancelled after " + i + " iterations");
return "Cancelled";
}
}
pi *= 4;
log("Calculated Pi: " + pi);
return Double.toString(pi);
}
}
  // This runs in the UI thread
private static void handleCalculatePiRequest() {
log("UI thread received calculate Pi request");
calculatePiTask = BackgroundTaskRunner.run(
() -> calculatePi(),
pi -> updateUI(pi),
error -> showError(error));
log("UI is now responsive again");
}

// This runs in the UI thread
private static void handleCancelCalculatePiRequest() {
log("UI thread received cancel request");
if (calculatePiTask != null) {
calculatePiTask.cancel();
updateUI("calculate Pi request cancelled");
calculatePiTask = null;
}
}

❌ Race condition could cause NullPointerException

Cook My Books Example I

public class CookMyBooks {
// ... log(), updateUI(), showError() unchanged
private static volatile Task<String> calculatePiTask = null;

private static String calculatePi() {
double pi = 0;
for (int i = 0; i < 100_000_000; i++) {
pi += Math.pow(-1, i) / (2 * i + 1);
// Prevent race condition by caching calculatePiTask.
Task<String> task = calculatePiTask;
if (task != null && task.isCancelled()) {
log("calculatePi was cancelled after "
+ i + " iterations");
return "Cancelled";
}
}
pi *= 4;
log("Calculated Pi: " + pi);
return Double.toString(pi);
}
}
 682ms JavaFX Application Thread UI thread received calculate Pi request
713ms JavaFX Application Thread UI is now responsive again
1593ms JavaFX Application Thread UI thread received cancel request
1594ms JavaFX Application Thread Updating UI: calculate Pi request cancelled
1595ms Thread-2 calculatePi was cancelled after 23144009 iterations

✅ CPU-bound loop stops early by checking isCancelled()

✅ UI updated directly in cancel handler

✅ Neither callback fires

✅ No race condition

Introducing Pitfall 3: Debounce Race Conditions

Four-panel pixel art illustration showing a debounce race condition. Panel 1: user types 'cake' in a recipe search bar, results loading, orange arrow labeled 'cake search running' travels rightward. Panel 2: user types 'cookies', results still loading, green arrow labeled 'cookies search running' travels rightward below the orange arrow. Panel 3: green arrow terminates with a downward arrow into the results list showing Chocolate Chip Cookies, Snickerdoodles, and Sugar Cookies highlighted in green — the correct results. Panel 4: orange arrow terminates with a downward arrow overwriting the results with Chocolate Cake, Carrot Cake, and Cheesecake highlighted in red — the wrong results. A red warning triangle and a confused user appear in panel 4.

Why "Debounce"?

Graphic showing waveform of button with bounce above debounced waveform. Push buttons are shown below.

Pitfall 3: Debounce Race Conditions

Search & Filter: Results update automatically as the user types. Each keystroke waits 300ms then runs a search on a background thread via BackgroundTaskRunner.

Consider what happens when the user types quickly:

  1. User types "cake" → 300ms passes → search fires on background thread
  2. User types "cookies" → 300ms passes → second search fires on background thread
  3. The "cookies" search finishes first and results are shown
  4. The "cake" search finishes second and overwrites the correct results

The user typed "cookies" but sees results for "cake".

This is a race condition — the result depends on which search happens to finish first, which is nondeterministic.

Poll: How can you ensure the most recent search's results are shown?

Poll Everywhere QR Code or Logo

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

https://pollev.com/espertus

Fixing the Debounce Race Condition: Generation Counter

Each time a new search starts, increment a generation counter. When results arrive, discard them if the generation no longer matches.

Shared counter: 1 → 2

Search 1 starts query: "cake" generation: 1

Search 2 starts query: "cookies" generation: 2

"cookies" arrives generation: 2 counter: 2
2 == 2 ✅ show

"cake" arrives generation: 1 counter: 2
1 != 2 ❌ discard

Each search captures the counter value when it starts. When results arrive, it checks whether the counter has moved on — if so, these results are stale.

Key Takeaways

  1. Blocking the FX thread freezes the UI — any slow operation must run on a background thread
  2. BackgroundTaskRunner handles the thread handoff — callable on background thread, callbacks on FX thread
  3. Three pitfalls to watch for in GA1:  — Use PauseTransition for timers, not ScheduledExecutorService  — Handle state transitions directly on cancel — cancel() does not trigger onFailure  — Use a generation counter to discard stale search results

Bonus Slide

Tweet from 'I Am Devloper': '10 Things You'll Find Shocking About Asynchronous Operations',
followed by an empty list of the numbers 1-10 in random order. A six-panel Spongebob Squarepants meme about asynchronous programming. Panel 1: Patrick (labeled C++) tells Squidward (labeled JS) 'I personally prefer asynchronous programming'. Panel 2: Squidward asks 'What does Asynchronous programming mean?' Panel 3: Spongebob (labeled Rust) tells Squidward 'It means he's afraid of multithreading'. Panel 4: Squidward replies 'No, it doesn't.' Panel 5: Patrick lists 'Shared pointer, Mutex, Move' while Squidward looks uncomfortable. Panel 6: Spongebob shouts 'Stop it Patrick, you're scaring him!'