唯细水静流

延迟加载原理与实现

本文介绍日常开发中可能用到的一种数据求值策略,延迟加载,也即惰性求值。介绍延迟加载的定义与原理,讲解使用该方式带来的好处以及合适的应用场景,最后介绍Java中的一些具体实现方案。

一、延迟加载的定义与原理

延迟加载是开发过程中灵活获取对象的一种求值策略,该策略在定义目标对象时并不会立即计算实际对象值,而是在该对象后续被实际调用时才去求值。在计算机科学中,延迟加载对应一个专门术语:惰性求值,其维基百科定义如下。

在编程语言理论中,惰性求值(Lazy Evaluation),又译为惰性计算、懒惰求值,也称为传需求调用(call-by-need),是计算机编程中的一个概念,目的是要最小化计算机要做的工作。延迟求值特别用于匿名式函数编程,在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。

简单来说,延迟加载就是指表达式只在必要时才求值,而非被赋给某个变量时立即求值。与惰性求值相对应的是及早求值,其维基百科定义如下。

及早求值(Eager evaluation)又译热切求值,也被称为贪婪求值(Greedy evaluation),是多数传统编程语言的求值策略。在及早求值中,表达式在它被约束到变量的时候就立即求值。这在简单编程语言中作为低层策略是更有效率的,因为不需要建造和管理表示未求值的表达式的中介数据结构。

二、延迟加载的好处与应用场景

对于及早求值,对象在定义时便已获取,若后续逻辑并未使用该对象,则会造成资源浪费,降低运行效率。而惰性求值会在数据真正被调用时去获取,若后续未调用则不获取,避免计算开销,若后续调用多次,也可通过存储计算结果的方式来实现结果复用,避免多次计算。延迟加载流程图如下所示。

延迟加载流程.jpg

对于业务场景中计算开销较大的数据对象,若其在后续逻辑中可能会根据不同的业务判断条件在不同作用域中被使用多次,也可能一次都不会被使用到,那就可以使用延迟加载来获取该对象。

延迟加载问题举例代码1.png

如上述代码举例,Heavy为计算开销较大的对象。

  • 若采用立即加载,在某些特定业务条件下,conditionA与conditionB均为false,此时最终并未使用Heavy对象,则会造成资源浪费,若代码改为在各自条件的作用域中单独获取Heavy对象,则有可能造成重复获取,也会造成多余计算。
  • 若采用延迟加载,当条件均不满足时,不会调用Heavy对象,此时也没有触发真正加载操作,当存在条件满足时,会在第一次调用对象时进行加载操作,后续如还需使用可复用该次加载结果,整个过程没有资源浪费。

三、延迟加载的实现

Java语言中并没有直接的延迟加载方法,但在Java8中引入的lambda表达式以及Supplier等函数式接口为我们实现延迟操作提供了很大的便捷性。可以通过增加一个间接层来实现,相当于使用Proxy模式,利用Supplier定义计算逻辑,把耗资源的运算过程放入Supplier的get方法中,并在Proxy对象中持有该Supplier实例,由Proxy对象来维护目标对象的实际加载与结果缓存,并确保安全性(如线程安全等)。一种可行的实现方案如下所示。

延迟加载实现代码1.png

其中Lazy即为外层Proxy对象,内部持有Supplier实例delegate,通过volatile和双重检验锁实现目标对象的单例效果,保证最多只加载一次,实现结果缓存复用,并确保多线程并发环境下的访问安全性。同时还提供了一系列便捷操作,如映射/扁平化映射/过滤等操作,且仍返回延迟对象,同样具有延迟加载效果,方便开发使用。

除了上述实现方案之外,延迟加载也可通过其它类似方式实现,例如下述代码,通过运行时改变内部Supplier变量的对象类型来判断是否已加载对象并返回,并采用synchronized同步方法来实现线程安全。

延迟加载实现代码2.png

还有一种实现更为简便,代码如下所示,利用Java中ConcurrentHashMap本身的同步机制来实现线程安全。

延迟加载实现代码3.png

参考

-------------本文结束 感谢您的阅读-------------