1. Introduction
1.导言
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.
通常,当我们使用需要执行昂贵或缓慢方法的资源(如数据库查询或 REST 调用)时,我们倾向于使用本地缓存或私有字段。一般来说,lambda 函数允许我们使用方法作为参数,并推迟或完全省略方法的执行。
In this tutorial, we’ll show different ways to initialize fields lazily with lambda functions.
在本教程中,我们将展示使用 lambda 函数懒散地初始化字段的不同方法。
2. Lambda Replacement
2.替换 Lambda
Let’s implement the first version of our own solution. As a first iteration, we’ll provide the LambdaSupplier class:
让我们实现自己解决方案的第一个版本。作为第一次迭代,我们将提供 LambdaSupplier 类:
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.
LambdaSupplier 通过延迟执行 Supplier.get() 来实现字段的懒初始化。如果 getData() 方法被多次调用,Supplier.get() 方法也会被多次调用。因此,该类的行为与 Supplier 接口完全相同。每次调用 getData() 方法时,都会执行底层方法。
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.
不出所料,我们的测试用例验证了 Supplier.get() 函数被调用两次。
3. Lazy Supplier
3.懒惰的供应商
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:
由于 LambdaSupplier 无法缓解多次调用问题,我们实现的下一个发展目标是确保昂贵方法的单次执行。LazyLambdaSupplier 将返回值缓存到一个私有字段,从而扩展了 LambdaSupplier 的实现:
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.
此实现会将返回值存储到私有字段 data,因此可以在连续调用中重复使用该值。
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.
在上述测试中,Supplier.get() 函数被调用了两次。为了实现这一点,ExecutorService 同时调用了两个调用 LazyLambdaSupplier.getData() 函数的线程。此外,我们添加到 mockedExpensiveFunction 中的 Thread.sleep() 调用保证了当两个线程调用 getData() 函数时,字段 data 仍为 null 。
4. Thread-Safe Solution
4.线程安全解决方案
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:
让我们结合目前所学的知识来编写 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.
要解释为什么这种方法是线程安全的,我们需要设想多个线程同时调用 getData() 方法。在 data.get() 调用不为空之前,线程确实会阻塞,并且执行将是顺序的。一旦 data 字段初始化完成,多个线程就可以同时访问该字段。
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.
乍一看,有人可能会认为 getData() 方法中的双重空检查是多余的,但事实并非如此。事实上,外部空值检查可确保当 data.get() 不是空值时,线程不会阻塞同步块。
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
5.结论
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.
在本文中,我们展示了使用 lambda 函数懒散地初始化字段的不同方法。通过使用这种方法,我们可以避免多次执行昂贵的调用,还可以推迟执行。我们的示例可以作为本地缓存或 项目 Lombok 的 lazy-getter 的替代方法。
As always, the source code for our examples is available over on GitHub.
与往常一样,我们示例的源代码可在 GitHub 上获取。