Reputation: 2755
I have a main Swing
app with class members BufferedImage lastCapturedImage
, ScheduledExecutorService executor
with 2 threads in the thread pool.
BufferedImage lastCapturedImage;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
...
executor.scheduleWithFixedDelay(imageCaptureRunnable, 100, 1000 / TARGET_FPS, TimeUnit.MILLISECONDS);
executor.schedule(roboticArmRunnable, 500, TimeUnit.MILLISECONDS);
The first Runnable
pulls images (BufferedImage
) from a webcam and updates a class instance lastCapturedImage
.
private final Runnable imageCaptureRunnable = new Runnable() {
@Override
public void run() {
lastCapturedImage = webcam.getImage();
}
};
The second Runnable
processes the image and controls a robotic arm. The image capturing rate is much faster than the robotic arm consumer and only the most current image is needed by the robotic arm consumer. How best do I share the image in a thread safe way?
After researching this topic, my solution is to wrap the image (lastCapturedImage
) in a synchronized
block in the roboticArmRunnable
's run()
method, and make a copy of the image like this:
private final Runnable roboticArmRunnable = new Runnable() {
@Override
public void run() {
while(true){
BufferedImage clonedCameraCapture;
synchronized (lastCapturedImage) {
clonedCameraCapture = copyImage(lastCapturedImage);
}
// process the clonedCameraCapture image and move the robotic arm
}
}
};
My understanding is that the synchronized
block will allow for the roboticArmRunnable
to completely make the copy of the image before the imageCaptureRunnable
is allowed to update the image. Am I doing this right??
Thanks in advance!
Upvotes: 2
Views: 1997
Reputation: 691685
No, your code isn't safe.
First of all, for synchronization to be correct, all the access to the sharedstate must be synchronized, and not just the read access.
Second: synchronizing on a non-final field is wrong: since the field can change, one thread will acquire the lock on the old value and the second thread will then be able to enter the same synchronized section because the field has changed.
You don't have any atomicity problem to solve here: writing and reading a reference is guaranteed to be atomic. You have a visibility problem to solve though: nothing guarantees that the write made by the image reader thread (that writes the reference) will be visible by the robotic arm thread (that reads the reference).
So all you need is to make the field volatile, or to wrap it inside an AtomicReference:
private volatile BufferedImage lastImage;
or
private AtomicReference<BufferedImage> lastImageRef;
...
// in image reader
lastImageRef.set(theNewImage);
...
// in robotic arm
BufferedImage lastImage = lastImageRef.get();
If you were still willing to solve the visibility problem using synchronization, you would have to do something like:
static class LastImageHolder
private BufferedImage lastImage;
public synchronized BufferedImage get() {
return lastImage;
}
public synchronized BufferedImage set(BufferedImage lastImage) {
this.lastImage = lastImage;
}
}
private LastImageHolder lastImageHolder = new LastImageHolder();
Upvotes: 4
Reputation: 65813
Functionality like this can be done without locks. Here's a OneOf
:
class OneOf<T> {
volatile int which = 0;
final T[] them;
public OneOf(T[] them) {
this.them = them;
}
public T get() {
return get(0);
}
public T get(int skip) {
return them[(which + skip) % them.length];
}
public void skip() {
which += 1;
which %= them.length;
}
}
You can now use get()
to get the current one, get(1)
to peek at the following one. In your case your image reader will choose it's image using get(1)
to begin filling the next image while your robot will read the image using get()
to get the current one.
Upvotes: -1