当前位置: 首页 > 科技观察

Java关于延迟加载的一些应用实践

时间:2023-03-12 06:26:59 科技观察

代码中的很多操作都是Eager的,比如当一个方法调用发生时,参数会被立即求值。一般来说,使用Eager方法使编码本身更简单,但使用Lazy方法通常意味着更好的效率。惰性初始化一般有几种场景:对于消耗资源较多的对象:这样既可以节省一些资源,又可以加快对象的创建速度,从而提高整体性能。有些数据在启动时是获取不到的:比如有些上下文信息可能只是在其他的拦截器或者处理中设置的,这样当前bean在加载的时候可能无法获取到对应变量的值。使用惰性初始化可以在实际调用时获取,通过延迟来保证数据的有效性。Java8中引入的lambda为我们实现延迟操作提供了极大的便利,比如Stream、Supplier等,这里举几个例子。LambdaSupplier通过调用get()方法实现具体对象的计算、生成和返回,而不是在定义Supplier时进行计算,从而达到_延迟初始化_的目的。但是在使用中,往往需要考虑并发的问题,即防止多次实例化,就像Spring的@Lazy注解一样。publicclassHolder{//首次调用heavy.get()时触发的默认同步方法privateSupplierheavy=()->createAndCacheHeavy();publicHolder(){System.out.println("Holdercreated");}publicHeavygetHeavy(){//第一次调用后,heavy已经指向了一个新的实例,所以以后不会执行synchronizedreturnheavy.get();}//...privatesynchronizedHeavycreateAndCacheHeavy(){//在方法中定义类,注意class加载时嵌套类的区别{//每次返回固定值returnheavyInstance;}}//第一次调用该方法满足Redirectheavy到一个新的Supplier实例if(!HeavyFactory.class.isInstance(heavy)){heavy=newHeavyFactory();}returnheavy.get();}}创建Holder实例时,Heavy实例仍未创建。下面我们假设三个线程会调用getHeavy方法,前两个线程同时调用,第三个线程稍后调用。前两个线程调用这个方法的时候,会调用createAndCacheHeavy方法,因为这个方法是同步的。于是第一个线程进入方法体,第二个线程开始等待。在方法体中,会先判断当前的heavy是否是HeavyInstance的实例。如果不是,则重对象将替换为HeavyFactory类型的实例。显然,当第一个线程执行判断时,重对象只是Supplier的一个实例,所以重对象会被替换为HeavyFactory的一个实例,此时重实例才真正被实例化。当第二个线程进入执行这个方法时,heavy已经是HeavyFactory的一个实例,所以会立即返回(即heavyInstance)。当第三个线程执行getHeavy方法时,由于此时的重对象已经是HeavyFactory的实例,所以会直接返回需要的实例(即heavyInstance),与同步方法createAndCacheHeavy无关。上面的代码实际上实现了一个轻量级的虚拟代理模式。保证了懒加载在各种环境下的正确性。还有一个更好理解的基于委托的实现:https://gist.github.com/taichi/6daf50919ff276aae74fimportjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;importjava.util.function.Supplier;publicclassMemoizeSupplierimplementsSupplier{finalSupplier委托;ConcurrentMap,T>map=newConcurrentHashMap<>(1);publicMemoizeSupplier(Supplierdelegate){this.delegate=delegate;}@OverridepublicTget(){//利用computeIfAbsent方法的特点,保证实例化方法只会在key不存在时调用一次,然后实现单例returnthis.map.computeIfAbsent(MemoizeSupplier.class,k->this.delegate.get());}publicstaticSupplierof(Supplierprovider){returnnewMemoizeSupplier<>(provider);}}和一个更复杂但更实用的CloseableSupplier:publicstaticclassCloseableSupplierimplementsSupplier,Serializable{privatestaticfinallongserialVersionUID=0L;privatefinalSupplierdelegate;privatefinalbooleanresetAfterClose;privatevolatiletransientbooleaninitialized;privatetransientTvalue;privateCloseableSupplier(Supplierdelegate,booleanresetAfterClose){this.delegate=delegate;this.resetAfterClose=resetAfterClose;}publicTget(){//经典单例实现if(!(this.initialized)){//注意是volatile修饰的,保证happens-before,t一定实例化完成synchronized(this){if(!(this.initialized)){//DoubleLockCheckTt=this.delegate.get();tthis.value=t;this.initialized=true;returnt;}}}//初始化后就直接再读取取值,不同步抢锁returnthis.value;}publicbooleanisInitialized(){returninitialized;}publicvoidifPresent(ThrowableConsumerconsumer)throwsX{synchronized(this){if(initialized&&this.value!=null){consumer.accept(this.value);}}}publicOptionalmap(Functionmapper){checkNotNull(mapper);同步(this){if(initialized&&this.value!=null){returnofNullable(mapper.apply(value));}else{returnempty();}}}publicvoidtryClose(){tryClose(i->{});}publicclose)throwsX{synchronized(this){if(initialized){close.accept(value);if(resetAfterClose){this.value=null;initialized=false;}}}}publicStringtoString(){if(initialized){return"MoreSuppliers.lazy("+get()+")";}else{return"MoreSuppliers.lazy("+this.delegate+")";}}}StreamStream中的每一个这些方法分为两类:中间方法(limit()/iterate()/filter()/map())和结束方法(collect()/findFirst()/findAny()/count())。会立即执行,只有调用end方法后才会从前到后触发整个调用链。在触发下一个中间方法之前,所有可以处理的元素都处理一次。例如:Listnames=Arrays.asList("Brad","Kate","Kim","Jack","Joe","Mike");finalStringfirstNameWith3Letters=names.stream().filter(name->length(name)==3).map(name->toUpper(name)).findFirst().get();System.out.println(firstNameWith3Letters);只有当findFirst()的end方法被触发时才会触发整个Stream链,每个元素依次经过filter()->map()->findFirst()后返回。所以filter()先处理第一个和第二个不满足条件的,继续处理第三个满足条件的,然后触发map()方法,最后将转换后的结果返回给findFirst()。所以filter()触发了_3_次,而map()触发了_1_次。OK,让我们来看一个实际问题,关于无限集合。Stream类型的一个特征是它们可以是无限的。这个和集合类型不一样,Java里面的集合类型肯定是有限制的。Stream之所以可以无穷大,也是因为Stream的“惰性”特性。Stream只会返回你需要的元素,不会一次性把整个无限集合返回给你。Stream接口中有一个静态方法iterate(),可以为你创建一个无限的Stream对象。它需要接受两个参数:publicstaticStreamiterate(finalTseed,finalUnaryOperatorf)其中seed表示这个无限序列的起点,UnaryOperator表示如何根据前一个元素得到下一个元素,比如第二个序列中的元素可以这样确定:f.apply(seed)。下面是从某个数开始计算,依次返回count个质数的例子:publicclassPrimes{publicstaticbooleanisPrime(finalintnumber){returnnumber>1&&//从2开始对数求平方根,判断该数是否能整除该值,即divisorIntStream.rangeClosed(2,(int)Math.sqrt(number)).noneMatch(divisor->number%divisor==0);}privatestaticintprimeAfter(finalintnumber){if(isPrime(number+1))//如果当前数是下一个数是素数,则直接返回值returnnumber+1;else//否则继续从下一个数据的后面找第一个素数返回,递归returnprimeAfter(number+1);}publicstaticListprimes(finalintfromNumber,finalintcount){returnStream.iterate(primeAfter(fromNumber-1),Primes::primeAfter).limit(count).collect(Collectors.toList());}//...}对于iterate和limit,它们只是一个中间操作,得到的对象是仍然是Stream类型。对于collect方法来说,它是一个结束操作,会触发中间操作得到想要的结果。如果使用非Stream方法,需要面临两个问题:第一,无法事先知道fromNumber之后count个素数的数值边界是什么。第二,不能用有限的集合来表示计算范围,不能计算超大的值,也就是不知道第一个。哪里是质数,需要提前算出第一个质数,然后用while处理count次,找到后面的质数。也许primes方法的实现会被分成两部分,使实现变得复杂。如果用Stream来实现流处理,无限迭代,指定截止条件,内部有一套机制可以保证实现和执行非常优雅。