Java延遲加載的最佳實(shí)踐應(yīng)用示例!

作者 |?S.L
代碼中的很多操作都是Eager的,比如在發(fā)生方法調(diào)用的時(shí)候,參數(shù)會(huì)立即被求值。總體而言,使用Eager方式讓編碼本身更加簡單,然而使用Lazy的方式通常而言,即意味著更好的效率。
延遲初始化
一般有幾種延遲初始化的場(chǎng)景:
對(duì)于會(huì)消耗較多資源的對(duì)象:這不僅能夠節(jié)省一些資源,同時(shí)也能夠加快對(duì)象的創(chuàng)建速度,從而從整體上提升性能。 某些數(shù)據(jù)在啟動(dòng)時(shí)無法獲取:比如一些上下文信息可能在其他攔截器或處理中才能被設(shè)置,導(dǎo)致當(dāng)前bean在加載的時(shí)候可能獲取不到對(duì)應(yīng)的變量的值,使用 延遲初始化可以在真正調(diào)用的時(shí)候去獲取,通過延遲來保證數(shù)據(jù)的有效性。
在Java8中引入的lambda對(duì)于我們實(shí)現(xiàn)延遲操作提供很大的便捷性,如Stream、Supplier等,下面介紹幾個(gè)例子。
Lambda
Supplier
通過調(diào)用get()方法來實(shí)現(xiàn)具體對(duì)象的計(jì)算和生成并返回,而不是在定義Supplier的時(shí)候計(jì)算,從而達(dá)到了延遲初始化的目的。但是在使用 中往往需要考慮并發(fā)的問題,即防止多次被實(shí)例化,就像Spring的@Lazy注解一樣。
public?class?Holder?{
????//?默認(rèn)第一次調(diào)用heavy.get()時(shí)觸發(fā)的同步方法
????private?Supplier?heavy?=?()?->?createAndCacheHeavy();?
????public?Holder()?{
????????System.out.println("Holder?created");
????}
????public?Heavy?getHeavy()?{
????????//?第一次調(diào)用后heavy已經(jīng)指向了新的instance,所以后續(xù)不再執(zhí)行synchronized
????????return?heavy.get();?
????}
????//...
????private?synchronized?Heavy?createAndCacheHeavy()?{
????????//?方法內(nèi)定義class,注意和類內(nèi)的嵌套class在加載時(shí)的區(qū)別
????????class?HeavyFactory?implements?Supplier<Heavy>?{
????????????//?饑渴初始化
????????????private?final?Heavy?heavyInstance?=?new?Heavy();?
????????????public?Heavy?get()?{
????????????????//?每次返回固定的值
????????????????return?heavyInstance;?
????????????}?
????????}
????????
????????//第一次調(diào)用方法來會(huì)將heavy重定向到新的Supplier實(shí)例
????????if(!HeavyFactory.class.isInstance(heavy))?{
????????????heavy?=?new?HeavyFactory();
????????}
????????return?heavy.get();
????}
}
當(dāng)Holder的實(shí)例被創(chuàng)建時(shí),其中的Heavy實(shí)例還沒有被創(chuàng)建。下面我們假設(shè)有三個(gè)線程會(huì)調(diào)用getHeavy方法,其中前兩個(gè)線程會(huì)同時(shí)調(diào)用,而第三個(gè)線程會(huì)在稍晚的時(shí)候調(diào)用。
當(dāng)前兩個(gè)線程調(diào)用該方法的時(shí)候,都會(huì)調(diào)用到createAndCacheHeavy方法,由于這個(gè)方法是同步的。因此第一個(gè)線程進(jìn)入方法體,第二個(gè)線程開始等待。在方法體中會(huì)首先判斷當(dāng)前的heavy是否是HeavyInstance的一個(gè)實(shí)例。如果不是,就會(huì)將heavy對(duì)象替換成HeavyFactory類型的實(shí)例。顯然,第一個(gè)線程執(zhí)行判斷的時(shí)候,heavy對(duì)象還只是一個(gè)Supplier的實(shí)例,所以heavy會(huì)被替換成為HeavyFactory的實(shí)例,此時(shí)heavy實(shí)例會(huì)被真正的實(shí)例化。等到第二個(gè)線程進(jìn)入執(zhí)行該方法時(shí),heavy已經(jīng)是HeavyFactory的一個(gè)實(shí)例了,所以會(huì)立即返回(即heavyInstance)。當(dāng)?shù)谌齻€(gè)線程執(zhí)行g(shù)etHeavy方法時(shí),由于此時(shí)的heavy對(duì)象已經(jīng)是HeavyFactory的實(shí)例了,因此它會(huì)直接返回需要的實(shí)例(即heavyInstance),和同步方法createAndCacheHeavy沒有任何關(guān)系了。
以上代碼實(shí)際上實(shí)現(xiàn)了一個(gè)輕量級(jí)的虛擬代理模式(Virtual Proxy Pattern)。保證了懶加載在各種環(huán)境下的正確性。
還有一種基于delegate的實(shí)現(xiàn)方式更好理解一些(github):
import?java.util.concurrent.ConcurrentHashMap;
import?java.util.concurrent.ConcurrentMap;
import?java.util.function.Supplier;
public?class?MemoizeSupplier<T>?implements?Supplier<T>?{
?final?Supplier?delegate;
?ConcurrentMap,?T>?map?=?new?ConcurrentHashMap<>(1);
?public?MemoizeSupplier(Supplier?delegate) ?{
??this.delegate?=?delegate;
?}
?@Override
?public?T?get()?{
?????//?利用computeIfAbsent方法的特性,保證只會(huì)在key不存在的時(shí)候調(diào)用一次實(shí)例化方法,進(jìn)而實(shí)現(xiàn)單例
??return?this.map.computeIfAbsent(MemoizeSupplier.class,
????k?->?this.delegate.get());
?}
?public?static??Supplier?of(Supplier?provider) ? {
??return?new?MemoizeSupplier<>(provider);
?}
}
以及一個(gè)更復(fù)雜但功能更多的CloseableSupplier:
public?static?class?CloseableSupplier<T>?implements?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()?{
????????????//?經(jīng)典Singleton實(shí)現(xiàn)
????????????if?(!(this.initialized))?{?//?注意是volatile修飾的,保證happens-before,t一定實(shí)例化完全
????????????????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(Function?super?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()) 結(jié)束方法(collect()/findFirst()/findAny()/count())
前者的調(diào)用并不會(huì)立即執(zhí)行,只有結(jié)束方法被調(diào)用后才會(huì)依次從前往后觸發(fā)整個(gè)調(diào)用鏈條。但是需要注意,對(duì)于集合來說,是每一個(gè)元素依次按照處理鏈條執(zhí)行到尾,而不是每一個(gè)中間方法都將所有能處理的元素全部處理一遍才觸發(fā) 下一個(gè)中間方法。比如:
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);
當(dāng)觸發(fā)findFirst()這一結(jié)束方法的時(shí)候才會(huì)觸發(fā)整個(gè)Stream鏈條,每個(gè)元素依次經(jīng)過filter()->map()->findFirst()后返回。所以filter()先處理第一個(gè)和第二個(gè)后不符合條件,繼續(xù)處理第三個(gè)符合條件,再觸發(fā)map()方法,最后將轉(zhuǎn)換的結(jié)果返回給findFirst()。所以filter()觸發(fā)了3次,map()觸發(fā)了1次。
好,讓我們來看一個(gè)實(shí)際問題,關(guān)于無限集合。
Stream類型的一個(gè)特點(diǎn)是:它們可以是無限的。這一點(diǎn)和集合類型不一樣,在Java中的集合類型必須是有限的。Stream之所以可以是無限的也是源于Stream「懶」的這一特點(diǎn)。
Stream只會(huì)返回你需要的元素,而不會(huì)一次性地將整個(gè)無限集合返回給你。
Stream接口中有一個(gè)靜態(tài)方法iterate(),這個(gè)方法能夠?yàn)槟銊?chuàng)建一個(gè)無限的Stream對(duì)象。它需要接受兩個(gè)參數(shù):
public static Stream iterate(final T seed, final UnaryOperator f)
其中,seed表示的是這個(gè)無限序列的起點(diǎn),而UnaryOperator則表示的是如何根據(jù)前一個(gè)元素來得到下一個(gè)元素,比如序列中的第二個(gè)元素可以這樣決定:f.apply(seed)。
下面是一個(gè)計(jì)算從某個(gè)數(shù)字開始并依次返回后面count個(gè)素?cái)?shù)的例子:
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))?//?如果當(dāng)前的數(shù)的下一個(gè)數(shù)是素?cái)?shù),則直接返回該值
????????????return?number?+?1;
????????else?//?否則繼續(xù)從下一個(gè)數(shù)據(jù)的后面繼續(xù)找到第一個(gè)素?cái)?shù)返回,遞歸
????????????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());
????}
????//...
}
對(duì)于iterate和limit,它們只是中間操作,得到的對(duì)象仍然是Stream類型。對(duì)于collect方法,它是一個(gè)結(jié)束操作,會(huì)觸發(fā)中間操作來得到需要的結(jié)果。
如果用非Stream的方式需要面臨兩個(gè)問題:
一是無法提前知曉fromNumber后count個(gè)素?cái)?shù)的數(shù)值邊界是什么 二是無法使用有限的集合來表示計(jì)算范圍,無法計(jì)算超大的數(shù)值
即不知道第一個(gè)素?cái)?shù)的位置在哪兒,需要提前計(jì)算出來第一個(gè)素?cái)?shù),然后用while來處理count次查找后續(xù)的素?cái)?shù)。可能primes方法的實(shí)現(xiàn)會(huì)拆成兩部分,實(shí)現(xiàn)復(fù)雜。如果用Stream來實(shí)現(xiàn),流式的處理,無限迭代,指定截止條件,內(nèi)部的一套機(jī)制可以保證實(shí)現(xiàn)和執(zhí)行都很優(yōu)雅。
往期推薦

