Background Computation with Threads
A shared resource is an object that more than one thread uses, such as the image object in the source's example.
When Two Threads Need the Same Object
A program that uses threads often has more than one piece of code running at the same time. Each of those pieces can reach for objects in the program. When two or more of them reach for the same object, that object is a shared resource. This article looks at one situation that threads create: a background computation. One thread does work on an object while another thread uses that same object for something else. The central question is how the program should handle that overlap so that the work stays correct.
As you read, keep two questions in mind. First, which object is being used by more than one thread? Second, what does the code wrap around that object? Answering both questions is the core skill this article teaches. You will apply it to a short program at the end of the article.
What Makes a Resource Shared
A shared resource is an object that more than one thread uses. The image object in the source's graphics example is the standard illustration: a background thread works on the image while another thread draws it to the screen.
Sharing is a property of the object's use, not of the object's type. An object becomes a shared resource when two or more threads can access it. Neither thread owns the object in the sense that the object belongs only to it. Because of that, a change made by one thread is visible to any other thread that reads the object afterward. This is why the source treats the image as a resource that needs synchronized access, rather than as a private value that each thread keeps for itself.
The diagram shows the pattern from the source. Two different threads both connect to the same image object. Neither thread holds a private copy of it. If the background thread changes the image, the GUI thread sees that change when it reads the image afterward. That is exactly what makes the image a resource requiring coordination.
Finding the Shared Object in a Program
Most of the work in synchronizing shared resources is noticing where sharing happens. The source gives a pattern you can apply to any multithreaded program. Step one is to list the threads. Step two is to list the objects that each thread uses. Step three is to look for any object that appears in more than one list. That object is the one that needs synchronized access.
- 1List every thread that runs in the program.
- 2For each thread, write down every object that thread reads or changes.
- 3Compare the lists and look for any object that appears in more than one list.
- 4Treat each object found in step three as a shared resource that needs synchronized access.
- image is a field that both methods can reach
computeInBackgroundruns in the background threadpaintComponentruns in the GUI thread- both methods name image inside synchronized parentheses
Identifying the Shared Object in the Canvas Program
In the Canvas program above, which object is shared by more than one thread?
- 1List the threads
The background thread runs
computeInBackground(). The GUI thread runspaintComponent(). - 2List the objects each thread uses
computeInBackground()uses image.paintComponent()uses image. Each method also uses the loop counter i and the standard output stream, but those are not used by both threads. - 3Find the overlap
image appears in both lists. The loop counter i is local to
computeInBackground(), so it is not shared. - 4Confirm with the synchronized form
Both methods write synchronized (image), so the program is protecting the object that both threads use.
The shared object is image. Both threads reach for it, and both synchronized blocks name it.
Reading the synchronized Statement
The source uses the form synchronized(object) { code block }. The object named in the parentheses is the shared resource. The braces wrap the code that uses that object. The statement therefore says two things at once: which object needs coordinated access, and which code is covered by that coordination.
The statement names the image itself. It does not name the screen or the thread that does the computation. That detail matters. The synchronized statement is written around the shared object, not around a thread. When you read a synchronized block, ask which object is in the parentheses and then list every statement inside the braces. Those statements are the code the form protects.
In the source, this statement appears inside paintComponent(), which copies the image to the screen, and the source says that copy is done in a synchronized statement. The Canvas program mirrors that structure. Both methods wrap their use of image in the same form, so both uses are covered by the same synchronization around the same object.
How Synchronized Access Orders the Threads
The source states that when multiple threads are involved, access to a shared resource should be synchronized. The reason becomes clear when you imagine the two threads running without that coordination. The background thread might be partway through writing the image while the GUI thread copies it to the screen. The GUI thread would then see some pixels updated and others not yet updated.
The diagram below is a hypothetical interleaving of the two threads, invented to illustrate the risk. It is not a trace of a real run. The GUI thread reads the image between two writes made by the background thread.
Read the sequence from top to bottom as an ordered list of messages. The first write completes. The GUI thread then reads the image at step 2, before the second write at step 3 has happened. The painted result contains row 0 updated and row 1 still old. That is the first step where the image's state stops matching what the programmer expected, so that is where a trace should look.
The synchronized form is designed to prevent this kind of overlap. When the background computation and the painting code both wrap their use of image in synchronized(image), the two uses are coordinated around that one object. The second thread to reach the block on the same object does not use the image until the first thread has finished with its block. The source describes the goal, which is synchronized access to the shared resource; the exact waiting behavior is what this form provides.
Mistakes Beginners Make with Shared Objects
Common mistake Treating a thread as the thing to protect
What it looks likeWriting synchronized(Thread.currentThread()) around image updatesWhy it failsThe source says the synchronized statement names the shared object, not the thread. A thread-based lock does nothing to stop another thread from reaching the image.
Do this insteadName the shared object, such as synchronized(image), in the parentheses.
Common mistake Synchronizing only one side of the access
What it looks likeWrapping the background computation in synchronized(image) but leaving paintComponent() unwrapped
Why it failsThe GUI thread can still read the image while the computation is writing to it, so the overlap is not covered.
Do this insteadWrap every block of code in every thread that uses the shared object, using the same object in the parentheses.
Common mistake Assuming an object is private because it was created in one method
What it looks likeBelieving a local variable is safe because it was created inside computeInBackground()
Why it failsAn object becomes shared as soon as more than one thread can reach it. Where it was created does not decide whether it is shared.
Do this insteadUse the listing pattern: check each thread's objects and look for overlap.
Common mistake Looking for the bug in the wrong place
What it looks likeRe-reading the loop that computes the pixels when the painted output is wrong
Why it failsThe problem may be where the GUI thread reads the image, not in how the image is computed.
Do this insteadTrace step by step, note which thread runs each step and which shared object it touches, and find the first step where the state stops matching your expectation.
Tracing a Program to Find Divergence
When a multithreaded program gives a result you did not expect, start by asking where the result first departed from what you predicted. Work through the program one step at a time. For each step, note which thread is running and which shared object it touches. The first step where the object's state no longer matches your expectation is the place to look. This is the method the source recommends, and it is the method the interleaving diagram above follows.
In the Canvas program, suppose you remove the synchronized block from paintComponent() but keep the one in computeInBackground(). Which thread's steps could now overlap with the writes to image, and what would you look for in the output to find the first divergence?
Hints
- Start by listing the objects
paintComponent()uses. - Ask which thread is running the unwrapped code.
- Compare the state of image at each step with what the program should produce.
Key Takeaways
- A shared resource is an object that more than one thread uses, such as the image object in the source's example.
- To find shared resources, list each thread's objects and look for any object that appears in more than one list.
- The form
synchronized(object){ code block } names the shared object in parentheses and wraps the code that uses it, so the name in the parentheses is the object that needs coordinated access. - When a multithreaded result is wrong, trace the program step by step, note which thread touches which shared object, and find the first step where the object's state departs from your expectation.