What can you learn?
- What problem virtual proxy design pattern solves
- How the basic virtual proxy pattern works = How to make the pattern thread-safe without compromising fast performance
Prerequisite
- Basic understanding of how lambda expression works
- Just a little bit of software development experience
What is Virtual Proxy Pattern?
It is a software design pattern used for lazy-loading heavy objects. Let's see an example.
import java.util.*;
interface Image {
public void displayImage();
}
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadImageFromDisk();
}
private void loadImageFromDisk() {
// ...
System.out.println("Loading "+filename);
}
public void displayImage() { System.out.println("Displaying "+filename); }
}
class ProxyImage implements Image {
private String filename;
private Image image;
public ProxyImage(String filename) { this.filename = filename; }
public void displayImage() {
if (image == null) {
image = new RealImage(filename); // Lazy allocation
}
image.displayImage();
}
}
class ProxyExample {
public static void main(String[] args) {
Image image1 = new ProxyImage("HiRes_10MB_Photo1");
Image image2 = new ProxyImage("HiRes_10MB_Photo2");
Image image3 = new ProxyImage("HiRes_10MB_Photo3");
image1.displayImage(); // needs loading
image2.displayImage(); // needs loading
image2.displayImage(); // already loaded
// image3 has not been loaded
}
}
In the example above, ProxyImage contains a RealImage object. So we can think of it as a placeholder for an RealImage.
But since RealImage instances are heavy weight, we only want to instantiate an actual RealImage instance only when it's needed.
This is what Virtual Proxy pattern solves. A RealImage instance is initialized when and only when displayImage is called.
Simple, right?
What is the problem with Virtual Proxy pattern?
There is one thing that is sadly making the code above unusable. That is, the code has a race condition.
Consider when a multiple threads are running concurrently, and they call displayImage on the same ProxyImage instance at once. Each thread thinks "It's the first time displayImage is called. I need to initialize it!".
This ends up calling the heavy operation many times. In the worst case scenario, the heavy operation could have non-read-only operation, leaving the program in an undefined state.
synchronized
to the rescue
Java conveniently supports locking with ease, just by using synchronized
keyword in the method signature.
We modify displayImage of ProxyImage class as follows:
public synchronized void displayImage() {
if (image == null) {
image = new RealImage(filename); // Lazy allocation with thread-safety
}
image.displayImage();
}
Now that we have locking in place, we achieved thread-safety. Yay! ... but it still has another concern.
Performance concern
Although displayImage method is now thread-safe, it suffers performance issue.
After the first call, there will be no mutation of image
field of ProxyImage, which means race condition only exists in the first invocation of the method.
To address issue, we can make use of Supplier
functional interface of java.
With Supplier
import java.util.*;
interface Image {
public void displayImage();
}
class ProxyImage implements Image {
private String filename;
private Supplier<Image> imageSupplier = () -> loadImage();
public ProxyImage(String filename) { this.filename = filename; }
private synchronized Image loadImage() {
class ImageFactory implements Supplier<Image> {
private final String filename;
private final Image imageInstance = new RealImage(filename);
public ImageFactory(String filename) {
this.filename = filename;
}
public Image get() { return imageInstance; }
}
if (!ImageFactory.class.isInstance(imageSupplier)) {
imageSupplier = new ImageFactory();
}
return imageSupplier.get();
}
public void displayImage() {
imageSupplier.get().displayImage();
}
}
Quite a lot of changes, eh? Let's break down the changes.
Supplier<Image>
- Supplier is an functional interface with
get
method that returns an instance of Image - Initially,
imageSupplier
is a synchronizedloadImage
function but it gets replaced after the first call
loadImage
-
loadImage
instantiates a ImageFactory, which is another image supplier type, defined withinloadImage
- replaces its supplier field
ImageFactory
- implements
Supplier<Image>
- finally instantiates a
RealImage
object and returns this instance fromget
method
In a nutshell, the supplier is only synchronized for the first call and it's replaced with a non-synchronized supplier thereafter.
That is it. You made it!
That was quite long of a refactor but certainly makes your code more robust and performant.
Top comments (0)