Java延迟加载的最佳实践应用示例!

共 2240字,需浏览 5分钟

 ·

2021-02-19 14:25

作者 | S.L

来源 | http://r6d.cn/abGzy

代码中的很多操作都是Eager的,比如在发生方法调用的时候,参数会立即被求值。总体而言,使用Eager方式让编码本身更加简单,然而使用Lazy的方式通常而言,即意味着更好的效率。

延迟初始化

一般有几种延迟初始化的场景:

  • 对于会消耗较多资源的对象:这不仅能够节省一些资源,同时也能够加快对象的创建速度,从而从整体上提升性能。
  • 某些数据在启动时无法获取:比如一些上下文信息可能在其他拦截器或处理中才能被设置,导致当前bean在加载的时候可能获取不到对应的变量的值,使用 延迟初始化可以在真正调用的时候去获取,通过延迟来保证数据的有效性。

在Java8中引入的lambda对于我们实现延迟操作提供很大的便捷性,如Stream、Supplier等,下面介绍几个例子。

Lambda

Supplier

通过调用get()方法来实现具体对象的计算和生成并返回,而不是在定义Supplier的时候计算,从而达到了延迟初始化的目的。但是在使用 中往往需要考虑并发的问题,即防止多次被实例化,就像Spring的@Lazy注解一样。

public class Holder {
    // 默认第一次调用heavy.get()时触发的同步方法
    private Supplier heavy = () -> createAndCacheHeavy(); 
    public Holder() {
        System.out.println("Holder created");
    }
    public Heavy getHeavy() {
        // 第一次调用后heavy已经指向了新的instance,所以后续不再执行synchronized
        return heavy.get(); 
    }
    //...

    private synchronized Heavy createAndCacheHeavy() {
        // 方法内定义class,注意和类内的嵌套class在加载时的区别
        class HeavyFactory implements Supplier<Heavy{
            // 饥渴初始化
            private final Heavy heavyInstance = new Heavy(); 
            public Heavy get() {
                // 每次返回固定的值
                return heavyInstance; 
            } 
        }
        
        //第一次调用方法来会将heavy重定向到新的Supplier实例
        if(!HeavyFactory.class.isInstance(heavy)) {
            heavy = new HeavyFactory();
        }
        return heavy.get();
    }
}

当Holder的实例被创建时,其中的Heavy实例还没有被创建。下面我们假设有三个线程会调用getHeavy方法,其中前两个线程会同时调用,而第三个线程会在稍晚的时候调用。

当前两个线程调用该方法的时候,都会调用到createAndCacheHeavy方法,由于这个方法是同步的。因此第一个线程进入方法体,第二个线程开始等待。在方法体中会首先判断当前的heavy是否是HeavyInstance的一个实例。如果不是,就会将heavy对象替换成HeavyFactory类型的实例。显然,第一个线程执行判断的时候,heavy对象还只是一个Supplier的实例,所以heavy会被替换成为HeavyFactory的实例,此时heavy实例会被真正的实例化。等到第二个线程进入执行该方法时,heavy已经是HeavyFactory的一个实例了,所以会立即返回(即heavyInstance)。当第三个线程执行getHeavy方法时,由于此时的heavy对象已经是HeavyFactory的实例了,因此它会直接返回需要的实例(即heavyInstance),和同步方法createAndCacheHeavy没有任何关系了。

以上代码实际上实现了一个轻量级的虚拟代理模式(Virtual Proxy Pattern)。保证了懒加载在各种环境下的正确性。

还有一种基于delegate的实现方式更好理解一些(github):

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;

public class MemoizeSupplier<Timplements Supplier<T{

 final Supplier delegate;
 ConcurrentMap, T> map = new ConcurrentHashMap<>(1);

 public MemoizeSupplier(Supplier delegate) {
  this.delegate = delegate;
 }

 @Override
 public T get() {
     // 利用computeIfAbsent方法的特性,保证只会在key不存在的时候调用一次实例化方法,进而实现单例
  return this.map.computeIfAbsent(MemoizeSupplier.class,
    k -> this.delegate.get())
;
 }

 public static  Supplier of(Supplier provider) {
  return new MemoizeSupplier<>(provider);
 }
}

以及一个更复杂但功能更多的CloseableSupplier:

public static class CloseableSupplier<Timplements Supplier<T>, Serializable {

        private static final long serialVersionUID = 0L;
        private final Supplier delegate;
        private final boolean resetAfterClose;
        private volatile transient boolean initialized;
        private transient T value;

        private CloseableSupplier(Supplier delegate, boolean resetAfterClose) {
            this.delegate = delegate;
            this.resetAfterClose = resetAfterClose;
        }

        public T get() {
            // 经典Singleton实现
            if (!(this.initialized)) { // 注意是volatile修饰的,保证happens-before,t一定实例化完全
                synchronized (this) {
                    if (!(this.initialized)) { // Double Lock Check
                        T t = this.delegate.get();
                        this.value = t;
                        this.initialized = true;
                        return t;
                    }
                }
            }
            // 初始化后就直接读取值,不再同步抢锁
            return this.value;
        }

        public boolean isInitialized() {
            return initialized;
        }

        public  void ifPresent(ThrowableConsumer consumer) throws X {
            synchronized (this) {
                if (initialized && this.value != null) {
                    consumer.accept(this.value);
                }
            }
        }

        public  Optional map(Functionsuper T, ? extends U> mapper) {
            checkNotNull(mapper);
            synchronized (this) {
                if (initialized && this.value != null) {
                    return ofNullable(mapper.apply(value));
                } else {
                    return empty();
                }
            }
        }

        public void tryClose() {
            tryClose(i -> { });
        }

        public  void tryClose(ThrowableConsumer close) throws X {
            synchronized (this) {
                if (initialized) {
                    close.accept(value);
                    if (resetAfterClose) {
                        this.value = null;
                        initialized = false;
                    }
                }
            }
        }

        public String toString() {
            if (initialized) {
                return "MoreSuppliers.lazy(" + get() + ")";
            } else {
                return "MoreSuppliers.lazy(" + this.delegate + ")";
            }
        }
    }

Stream

Stream中的各种方法分为两类:

  • 中间方法(limit()/iterate()/filter()/map())
  • 结束方法(collect()/findFirst()/findAny()/count())

前者的调用并不会立即执行,只有结束方法被调用后才会依次从前往后触发整个调用链条。但是需要注意,对于集合来说,是每一个元素依次按照处理链条执行到尾,而不是每一个中间方法都将所有能处理的元素全部处理一遍才触发 下一个中间方法。比如:

List names = Arrays.asList("Brad""Kate""Kim""Jack""Joe""Mike");

final String firstNameWith3Letters = names.stream()
    .filter(name -> length(name) == 3)
    .map(name -> toUpper(name))
    .findFirst()
    .get();

System.out.println(firstNameWith3Letters);

当触发findFirst()这一结束方法的时候才会触发整个Stream链条,每个元素依次经过filter()->map()->findFirst()后返回。所以filter()先处理第一个和第二个后不符合条件,继续处理第三个符合条件,再触发map()方法,最后将转换的结果返回给findFirst()。所以filter()触发了3次,map()触发了1次。

好,让我们来看一个实际问题,关于无限集合。

Stream类型的一个特点是:它们可以是无限的。这一点和集合类型不一样,在Java中的集合类型必须是有限的。Stream之所以可以是无限的也是源于Stream「懒」的这一特点。

Stream只会返回你需要的元素,而不会一次性地将整个无限集合返回给你。

Stream接口中有一个静态方法iterate(),这个方法能够为你创建一个无限的Stream对象。它需要接受两个参数:

public static Stream iterate(final T seed, final UnaryOperator f)

其中,seed表示的是这个无限序列的起点,而UnaryOperator则表示的是如何根据前一个元素来得到下一个元素,比如序列中的第二个元素可以这样决定:f.apply(seed)。

下面是一个计算从某个数字开始并依次返回后面count个素数的例子:

public class Primes {
    
    public static boolean isPrime(final int number) {
        return number > 1 &&
            // 依次从2到number的平方根判断number是否可以整除该值,即divisor
            IntStream.rangeClosed(2, (int) Math.sqrt(number))
                .noneMatch(divisor -> number % divisor == 0);
    }
    
    private static int primeAfter(final int number) {
        if(isPrime(number + 1)) // 如果当前的数的下一个数是素数,则直接返回该值
            return number + 1;
        else // 否则继续从下一个数据的后面继续找到第一个素数返回,递归
            return primeAfter(number + 1);
    }
    public static List primes(final int fromNumber, final int count) {
        return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
            .limit(count)
            .collect(Collectors.toList());
    }
    //...
}

对于iterate和limit,它们只是中间操作,得到的对象仍然是Stream类型。对于collect方法,它是一个结束操作,会触发中间操作来得到需要的结果。

如果用非Stream的方式需要面临两个问题:

  • 一是无法提前知晓fromNumber后count个素数的数值边界是什么
  • 二是无法使用有限的集合来表示计算范围,无法计算超大的数值

即不知道第一个素数的位置在哪儿,需要提前计算出来第一个素数,然后用while来处理count次查找后续的素数。可能primes方法的实现会拆成两部分,实现复杂。如果用Stream来实现,流式的处理,无限迭代,指定截止条件,内部的一套机制可以保证实现和执行都很优雅。

喜欢本文的朋友,欢迎点击下方卡片
关注我,订阅更多精彩内容


往期推荐

不容错过的灰度发布系统架构设计

还在封装各种 Util 工具类?这个神级框架帮你解决所有问题!

阿里开源台柱 Ant Design 源码仓库被删了...

GitHub最最最火的开源爬虫工具箱,一爬就取

明天即将开工,把今年的Flag加到头像上,时刻鞭策自己吧!

情人节微信红包数据公布,你离海王与海后有多远...



浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报