DEV Community

Kai Sawamoto
Kai Sawamoto

Posted on

Lazy Loading with Virtual Proxy Pattern | Thread-safety and beyond

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 synchronized loadImage function but it gets replaced after the first call

loadImage

  • loadImage instantiates a ImageFactory, which is another image supplier type, defined within loadImage
  • replaces its supplier field

ImageFactory

  • implements Supplier<Image>
  • finally instantiates a RealImage object and returns this instance from get 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)