Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

Frequently, when we work with resources that require the execution of expensive or slow methods, such as database queries or REST calls, we tend to use local caches or private fields. In general, lambda functions allow us to use methods as arguments and to defer a method’s execution or omit it completely.

In this tutorial, we’ll show different ways to initialize fields lazily with lambda functions.

2. Lambda Replacement

Let’s implement the first version of our own solution. As a first iteration, we’ll provide the LambdaSupplier class:

public class LambdaSupplier<T> {

    protected final Supplier<T> expensiveData;

    public LambdaSupplier(Supplier<T> expensiveData) {
        this.expensiveData = expensiveData;
    }

    public T getData() {
        return expensiveData.get();
    }
}

LambdaSupplier achieves the lazy initialization of a field via the deferred Supplier.get() execution. If the getData() method is called multiple times, the Supplier.get() method is also called multiple times. Thus, this class behaves exactly the same as the Supplier interface. The underlying method is executed every time the getData() method is called.

To showcase this behavior, let’s write a unit test:

@Test
public void whenCalledMultipleTimes_thenShouldBeCalledMultipleTimes() {
    @SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenReturn("expensive call");
    LambdaSupplier<String> testee = new LambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    testee.getData();
    testee.getData();
    Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
        .get();
}

As expected, our test case verifies that the Supplier.get() function is invoked two times.

3. Lazy Supplier

Since the LambdaSupplier doesn’t mitigate the multiple calls issue, the next evolution of our implementation aims to guarantee the single execution of the expensive method. The LazyLambdaSupplier expands on the LambdaSupplier‘s implementation by caching the returned value to a private field:

public class LazyLambdaSupplier<T> extends LambdaSupplier<T> {

    private T data;

    public LazyLambdaSupplier(Supplier<T> expensiveData) {
        super(expensiveData);
    }

    @Override
    public T getData() {
        if (data != null) {
            return data;
        }
        return data = expensiveData.get();
    }

}

This implementation stores the returned value to the private field data so the value can be re-used in consecutive calls.

The following test case verifies that the new implementation doesn’t make multiple calls when called sequentially:

@Test
public void whenCalledMultipleTimes_thenShouldBeCalledOnlyOnce() {
    @SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenReturn("expensive call");
    LazyLambdaSupplier<String> testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    testee.getData();
    testee.getData();
    Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
        .get();
}

Essentially, the template of this test case is the same as our previous test case. The important difference is that in the second case, we verify that the mocked function was called only once.

To show that this solution isn’t thread-safe, let’s write a test case with concurrent executions:

@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledMultipleTimes() throws InterruptedException {
    @SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenAnswer((Answer) invocation -> {
            Thread.sleep(1000L);
            return "Late response!";
        });
    LazyLambdaSupplier testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();

    ExecutorService executorService = Executors.newFixedThreadPool(4);
    executorService.invokeAll(List.of(testee::getData, testee::getData));
    executorService.shutdown();
    if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        executorService.shutdownNow();
    }

    Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
        .get();
}

The Supplier.get() function is invoked twice in the above test. To make that happen, the ExecutorService simultaneously invokes two threads that call the LazyLambdaSupplier.getData() function. Furthermore, the Thread.sleep() call we added to the mockedExpensiveFunction guarantees that the field data will still be null when the getData() function is called by both threads.

4. Thread-Safe Solution

Finally, let’s tackle the thread safety limitation that we demonstrated above. To accomplish that, we’ll need to use synchronized data access and a thread-safe value wrapper, namely the AtomicReference.

Let’s combine what we’ve learned so far to write the LazyLambdaThreadSafeSupplier:

public class LazyLambdaThreadSafeSupplier<T> extends LambdaSupplier<T> {

    private final AtomicReference<T> data;

    public LazyLambdaThreadSafeSupplier(Supplier<T> expensiveData) {
        super(expensiveData);
        data = new AtomicReference<>();
    }

    public T getData() {
        if (data.get() == null) {
            synchronized (data) {
                if (data.get() == null) {
                    data.set(expensiveData.get());
                }
            }
        }
        return data.get();
    }

}

To explain why this approach is thread-safe, we need to imagine that multiple threads call the getData() method all at once. Threads will indeed block and the execution will be sequential until the data.get() call is not null. Once the data field initialization is complete, then multiple threads can access it concurrently.

At first glance, someone might argue that the double null check in the getData() method is redundant, but it’s not. In fact, the outer null check ensures that when the data.get() is not null, the threads do not block on the synchronized block.

To verify that our implementation is thread-safe, let’s provide a unit test in the same fashion as we did for the previous solutions:

@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledOnlyOnce() throws InterruptedException {
    @SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenAnswer((Answer) invocation -> {
            Thread.sleep(1000L);
            return "Late response!";
        });
    LazyLambdaThreadSafeSupplier testee = new LazyLambdaThreadSafeSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();

    ExecutorService executorService = Executors.newFixedThreadPool(4);
    executorService.invokeAll(List.of(testee::getData, testee::getData));
    executorService.shutdown();
    if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        executorService.shutdownNow();
    }

    Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
        .get();
}

5. Conclusion

In this article, we showed different ways to lazily initialize fields using lambda functions. By using this approach, we can avoid executing expensive calls more than once and also defer them. Our examples can be used as an alternative to local caches or Project Lombok‘s lazy-getter.

As always, the source code for our examples is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.