Context
I have a class called ImageLoader.
When I call ImageLoader's getPicture( pictureID ), if the picture is not cached, I store pictureID in an instance variable, create a new thread which eventually calls my callback() function in this same ImageLoader class. callback() then uses this pictureID as the key to cache the picture.
Problem
Do you see the pickle?
If I call getPicture( somePictureID ) twice in a row, and the callback() function for the first call hasn't happened yet, I will overwrite the previous pictureID which the callback() function will use. (See the code below if you still don't see the pickle.)
Now I know you're thinking, why don't I just add a little synchronization on the variable to make it thread-safe?
Because who knows how long the thread querying the picture will take, and that could really slow down the whole process of loading multiple images.
How I'm Thinking of Solving It
So my bright idea is to create an inner class that servers as a slave and only has a single use. I execute some function in this slave class once, and I never reuse the object.
Question
Is this an appropriate approach to solving this kind of problem? I feel like this might fall into some pattern I'm not aware of. If I'm totally off the mark with this one, can you suggest another solution?
Simplified Code
//the instance variable with a race condition
private int pictureID
//loads the image associated with the pictureID
public void getPicture( int pictureID )
{
Bitmap bitmap = cache.get( pictureID );
if ( bitmap != null )
{
//load image
return;
}
int post_params[] = { pictureID, /* more parameters */ };
this.pictureID = pictureID; //PROBLEM: race condition!
new HttpRequest( this ).execute( post_params ); //starts new thread and queries server
}
//gets called when the query finishes, json contains my image
public void callback( JSONObject json )
{
Bitmap bitmap = getBitmap( json ); //made up method to simplify code
cache.put( this.pictureID, bitmap ); //PROBLEM: race condition!
//load image
}
To me this looks like a classic case of the Memoizer pattern, and the easiest way to use it is probably Guava's CacheBuilder, something like:
private LoadingCache<Integer, Image> cache = CacheBuilder.newBuilder().
build(new CacheLoader<Integer, Image>() {
@Override
public Image load(Integer key) throws Exception {
return httpGetImageExpensively(key);
}
});
public Image getPicture(int pictureId) {
return cache.getUnchecked(pictureId); // blocks until image is in cache
}
Now, the HTTP request will happen in the first thread that calls getPicture() for a given ID; subsequent callers with that ID will block until the first thread has put the image in cache, but you can make multiple concurrent requests for different IDs.
Things to think about:
cache.get() method throws a (checked) ExecutionException, which is why I'm using getUnchecked() instead, but that just throws an (unchecked) UncheckedExecutionException instead. If possible, I would do most of the error handling in httpGetImageExpensively(), and think through the possible cases there (bad ID, DNS failure, etc.) but the callers of getPicture() will still need to deal with that somehow.getPicture() with something a little fancier that takes an image handler callback object or lambda, and then internally put the actual calls to httpGetImageExpensively() on an ExecutorService, so getPicture() can return immediately. (Depending on the application, there may be other issues, e.g. in a Swing app it might be desirable to invoke the callback on the event dispatch thread.)If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With