Managing Shared State Between Threads
Multiple threads can be coordinated by tracking how many of them are still running in a shared counter named threadsRunning.
Why a Program Needs to Know When Work Is Done
When a program starts several threads to do work at the same time, the rest of the program often needs to know one simple fact: have all of them finished? Without an answer to that question, the program cannot decide when to stop a busy indicator, reset a button, or release the objects that hold the threads. Thread synchronization is the practice of managing multiple threads so that they complete their tasks without interfering with each other or with the main flow of the program.
This article follows one common approach described in the source material. Instead of asking each thread to report directly to the interface, each finishing thread calls a single method named threadFinished. That method updates a shared counter, and only the thread that brings the counter to zero performs the final cleanup. As you read, keep one question in mind: at what point does the program know the work is over, and what has to happen before that point for the answer to be correct?
The Shared Counter threadsRunning
Multiple threads can be coordinated by tracking how many of them are still running in a shared counter named threadsRunning. The counter records how many threads have not yet finished, and every finishing thread updates that same record.
Because the record is shared, every thread reads and writes the same value. The source describes the variable as the number of threads still running. Each call to threadFinished begins by subtracting one from threadsRunning. Since the counter starts at the number of threads launched and drops by one for each thread that finishes, its value at any moment tells you how much work remains.
The diagram uses three threads so the countdown is easy to follow. The counter moves one step for each finishing thread, and the only value that matters for the completion decision is the final one, zero. The diagram is a simplified model: it shows one ordered sequence of finishes, while the real program may have finishes happening close together. That interleaving is what makes the order of operations, covered next, so important.
The Decrement Comes First
Each finishing thread performs its update before it performs any test. In the source's method, the first statement subtracts one from threadsRunning. Only after that does the method check whether the value has reached zero. This ordering is the single most important fact for reading the completion logic, and it is where many learners lose track of the flow.
- The first line is an update: it changes the shared counter.
- The if line is the test: it is the synchronization decision.
- The indented lines are reactions: they run only when the test passes.
A useful way to read any line of this method is to ask whether it is an update, a test, or a reaction to the test. The decrement is an update. The if statement is the test. Everything inside the braces is a reaction that runs only when the test passes. Sorting lines this way makes it easier to see where a result diverges from what you expected.
Tracing the Completion Path
What do you think happens?
Three threads finish one after another. When the completion branch runs, what value does threadsRunning hold?
Reveal answer
0, because the branch runs only when the decrement brings the value to zero.
The source states that only the call that brings the value to zero triggers the completion branch. Every thread decrements first, so the counter reaches zero exactly once, on the last finish.
Follow the flowchart from top to bottom. The thread calls threadFinished, the counter is decremented, and then the zero test runs. The test is the line that makes the all-finished decision. If the count is not zero, the method ends and the other threads keep working. If the count is zero, the reactions run in the order the source describes: the start button is relabeled and enabled, running is set to false, workers is set to null, and the thread count selector is re-enabled.
The Zero Test as the Synchronization Signal
The check threadsRunning == 0 is the synchronization decision. The source states the rule plainly: when threadsRunning equals 0, all threads have finished. The check does not ask whether a particular thread is done. It asks only whether the shared count has reached its end. This is why the count is the mechanism and the check is the signal.
Resetting the Interface After Completion
The cleanup branch is where thread status reaches the user. The source describes two interface elements that depend on completion. The start button is relabeled and enabled so the user can start again, and the thread count selector is enabled so the user can change how many threads to run. Both changes happen only inside the branch that runs when every thread has finished. Before that point, the interface is still showing work in progress.
The diagram shows the same branch from the data side. One condition, the counter reaching zero, sends a signal that touches four places. Two of them are visible to the user: the start button and the thread count selector. The other two are program state: the running flag and the workers array. None of these four changes happens while threads are still finishing, because all of them sit inside the branch.
Tracing a Result That Goes Wrong
What happens if the test runs before the decrement
Suppose a developer reorders the method so the zero test runs before the decrement, with two threads still running when the last one finishes. Does the completion branch run at the right time?
- 1Start state
threadsRunningis 1, because one thread is still working and the other has finished. - 2Test first
The zero test reads 1. The condition is false, so the completion branch does not run.
- 3Decrement second
The method subtracts one, so
threadsRunningbecomes 0. Nothing in the method checks again. - 4Result
The counter reached zero but the branch never ran. The start button stays disabled, the selector stays locked, and workers is never cleared.
The completion decision diverged from what was expected. The fix is to restore the order: decrement first, then test.
Mistakes When Reading Completion Logic
Common mistake Assuming every thread runs the cleanup
Why it failsOnly the call that brings the value to zero triggers the completion branch. Other threads finish their own call and simply return.
Do this insteadRead the if statement as a gate that most calls fail and only the last call passes.
Common mistake Thinking the test asks whether a specific thread is done
Why it failsThe check asks only whether the shared count has reached zero. It does not identify which thread finished last.
Do this insteadTreat threadsRunning as the signal and the zero test as the reading of that signal.
Common mistake Reading the decrement and the test as one step
What it looks likeSwapping the two lines so the test happens before the subtraction.
Why it failsThe test then sees the value before the last finish is recorded, so the completion branch can be skipped.
Do this insteadKeep the decrement first, as the source describes, and confirm that order whenever a completion result looks wrong.
Common mistake Expecting the interface to update before all threads finish
Why it failsThe start button, running flag, workers array, and selector are all changed only inside the branch that runs after the count reaches zero.
Do this insteadBefore the branch runs, the interface should still show work in progress.
Practice Tracing the Counter
Four threads are started. Trace threadsRunning through the four finishes. For each finish, write the value after the decrement and say whether the completion branch runs. Then say which single change would fix a version of the method where the test is placed before the decrement.
Hints
- Start at 4 and subtract one for each finishing thread.
- The completion branch runs only on the call that produces zero.
- If the test comes first, it compares a value that is one higher than the one the decrement will leave behind.
Key Takeaways
- A shared counter named
threadsRunningtracks how many threads are still working, and every finishing thread updates it. - Each finishing thread decrements the counter first, and only the call that brings the value to zero triggers the completion branch.
- The check
threadsRunning== 0 is the all-finished decision, and the branch it guards resets the start button, sets running to false, clears workers, and re-enables the thread count selector. - When a completion result is wrong, check the order of the decrement and the zero test before anything else.