java原始碼學習-Spliterator

dust1發表於2018-10-04

上一篇文章檢視了jdk關於Stream流的建立,其中用於資料儲存的便是Spliterator,這篇文章將會對Spliterator的原始碼進行檢視,來看看在流中的資料是如何儲存的。

Spliterator是一個介面,它擁有子介面

Spliterator.OfDouble, Spliterator.OfInt, Spliterator.OfLong, Spliterator.OfPrimitive<T,T_CONS,T_SPLITR>

這些子介面通過原始碼可以發現是對Spliterator的繼承

public interface OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>
            extends Spliterator<T> {
            /* other code */
}
複製程式碼

這就是巢狀介面。這幾個子類通過繼承來擴充套件介面。在《thinking in java》一書對這個方法的介紹:

通過繼承,可以很容易地在介面中新增新的方法宣告,還可以通過繼承在新介面中組合數個介面。這兩種情況都可以獲得新的介面。

而上面的程式碼就是對多個介面進行繼承產生新的介面。

所有對Spliterator的實現類:

Spliterators.AbstractDoubleSpliterator, Spliterators.AbstractIntSpliterator, Spliterators.AbstractLongSpliterator, Spliterators.AbstractSpliterator

這裡發現所有Spliterator的實現類都是位於Spliterators下面的內部類,那麼可以發現它的設計如下:

定義一個介面A,而對該介面的擴充套件在它內部實現。而對該介面的實現則建立對應的類As中的內部類進行實現

在文件中對於該類作用的主要作用介紹為:

用於遍歷和分割槽源元素的物件。 Spliterator所涵蓋的元素源可以是例如陣列,集合,IO通道或生成器函式。並且還能對其某些元素進行拆分使得可以採用並行操作(這裡我覺的和分支-合併框架有點類似)。 此外還增加了一組特徵值表示ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED.Spliterator客戶端可以使用這些來控制,專門化或簡化計算。例如,Collection的Spliterator將報告SIZED,Set的Spliterator將報告DISTINCT,而SortedSet的Spliterator也會報告SORTED。特徵報告為簡單的聯合位集。一些特徵還限制了方法行為;例如,如果ORDERED,遍歷方法必須符合其記錄的順序。未來可能會定義新特徵,因此實現者不應將意義分配給未列出的值。看起來很想Iterator,但他比Iterator效率更高,適用範圍更廣。與迭代器一樣,Spliterators用於遍歷源的元素。 除了順序遍歷之外,Spliterator API還支援有效的並行遍歷,支援分解以及單元素迭代。 此外,通過Spliterator訪問元素的協議旨在實現比Iterator更小的每元素開銷,並避免使用hasNext()和next()的單獨方法所涉及的固有競爭。

定義的方法

  • int characteristics() - 返回Spliterator資料對應的特徵值。根據文件給出的事例,可知它返回的是多個特徵碼的按位或的結果。
public int characteristics() {
    return ORDERED | SIZED | IMMUTABLE | SUBSIZED;
}
複製程式碼
  • default void forEachRemaining(Consumer<? super T> action)方法 - 在當前執行緒中按照給定的操作對所有元素執行一遍
  • long estimateSize() - 返回forEachRemaining(Consumer<? super T> action)在執行前將要遇到的元素數量的估算值,如果太高就返回Integer.MAX_VALUE。
  • boolean tryAdvance(Consumer<? super T> action) - 如果存在剩餘元素,則對其執行給定的操作

這裡和第一個方法並不相同,根據文件給出的事例,該方法事例實現如下:

public boolean tryAdvance(Consumer<? super T> action) {
    if (origin < fence) {
        action.accept((T) array[origin]);
        origin++;
        return true;
    } else {
        return false;
    }
}
/*
其中array為Object[]用來儲存資料,origin為開始執行的下標,fence為結束執行的下標。
從中我們可以看到該方法只執行一次,如果成功就返回true否則返回false.
*/
複製程式碼
  • Spliterator trySplit() - 如果可以對此spliterator進行分割槽,則返回Spliterator覆蓋元素,這些元素在從此方法返回時將不被此Spliterator覆蓋。這樣看起來有點難懂,關於這個方法其實就是負責並行的方法,根據文件原文:A Spliterator may also partition off some of its elements (using trySplit()) as another Spliterator, to be used in possibly-parallel operations. 可知,該方法用我的理解就是對該物件的元素進行切分,用切分出來的部分建立一個新的Spliteraor物件以方便進行並行操作.而呼叫該方法的執行緒會將返回的Spliterator交給另一個新的執行緒,新的執行緒又可以繼續分割槽。這樣使得程式的執行速度大大提高。如果沒有公共引數的話還能完全不考慮死鎖這種資源佔用的問題。

對於該分成多少個執行緒這點,如果執行緒池中執行緒數量過多,最終他們會競爭稀缺的處理器和記憶體資源,浪費大量的時間在上下文切換上。反之,如果執行緒的數目過少,那麼處理器的一些核可能無法充分利用。關於這點,在《Java併發程式設計實戰》一書中Brian Goetz提出的解決辦法:

執行緒池大小與處理器的利用率紙幣可以使用下面的公式進行估算:
N(threads) = N(cpu) * U(cpu) * (1 + W/C)
其中:

  • N(cpu)是處理器的核的數目,可以通過Runtime.getRuntime().availableProcessors()得到
  • U(cpu)是期望的CPU利用率(該值應該介於0到1之間)
  • W/C是等待時間與計算時間的比率 例:如果應用99%的時間都在等待請求的相應,所以估算出來的W/C比率為100
  • default Comparator<? super T> getComparator() - 如果此Spliterator的源由比較器SORTED,則返回Comparator。而上文說了當資料結構為SortedSet的時候比較器會為SORTED。
  • default long getExactSizeIfKnown() - 如果此Spliterator為SIZED則返回estimateSize()的便捷方法,否則為-1。當比較器為SIZED的時候資料來源為Collection,為有序的集合,此時該方法的作用與estimateSize()相同.而原始碼也是如此:
default long getExactSizeIfKnown() {
    return (characteristics() & SIZED) == 0 ? -1L : estimateSize();
}
複製程式碼
  • default boolean hasCharacteristics(int characteristics) - 如果此Spliterator的特性包含所有給定特徵,則返回true。
default boolean hasCharacteristics(int characteristics) {
    return (characteristics() & characteristics) == characteristics;
}
複製程式碼

從給出的方法來看Spliterator這個介面的主要功能是儲存資料以及更加方便地並行處理資料,他把後續對資料的操作通過函式分離到了外部。這種設計好處顯而易見了,我們可以對資料前後進行多次不同的操作。這既視感像什麼呢?沒錯,就是

Arrays.asList(1, 2, 3).stream()
    .filter(i -> i % 2 == 0)
    .sort(Integer::compare)
    .collect(Collectors.toList());
複製程式碼

這樣的filter和sort之類的一系列操作都可以實現了。

Spliterator子介面

  • static interface Spliterator.OfDouble - 浮點數專用Spliterator
  • static interface Spliterator.OfInt - 整數專用Spliterator
  • static interface Spliterator.OfLong - 長整型數專用Spliterator
  • static interface Spliterator.OfPrimitive<T,T_CONS,T_SPLITR extends Spliterator.OfPrimitive<T,T_CONS,T_SPLITR>> - 專門用於原始值的Spliterator

以上幾個子介面是Spliterator為基礎資料型別提供的原始子型別特化,其目的是為了避免java裝箱操作所產生的多餘損耗。

裝箱操作 - 當java中基本資料型別和轉為其對應的包裝型別的時候就是裝箱操作。但是會產生多餘的執行損耗.如int轉為Integer

原始型別特化 - 函式介面java.util.function.Consumer的型別特例化,也就是限定accept引數型別.例如int的原始型別特里化為java.util.function.IntConsumer其accept的引數為int

其中對引數為Consumer<T>型別的方法過載,以及覆蓋。具體例子如下:

@Override
default boolean tryAdvance(Consumer<? super Integer> action) {
    if (action instanceof IntConsumer) {
        return tryAdvance((IntConsumer) action);
    }
    else {
        if (Tripwire.ENABLED)
            Tripwire.trip(getClass(),
                          "{0} calling Spliterator.OfInt.tryAdvance((IntConsumer) action::accept)");
        return tryAdvance((IntConsumer) action::accept);
    }
}
複製程式碼

其中Tripwire.ENABLED用於檢測java類中無意中使用裝箱的實用程式類。根據是否開啟系統屬性org.openjdk.java.util.stream.tripwire來檢測。通常情況下是關閉的。 具體程式碼如下:

private static final String TRIPWIRE_PROPERTY = "org.openjdk.java.util.stream.tripwire";

/** Should debugging checks be enabled? */
static final boolean ENABLED = AccessController.doPrivileged(
        (PrivilegedAction<Boolean>) () -> Boolean.getBoolean(TRIPWIRE_PROPERTY));
複製程式碼

doPrivileged(PrivilegedAction action);接受的是函式介面。 但是它本身使用native修飾的,也就是它的實現位於其他語言中.

@CallerSensitive
public static native <T> T doPrivileged(PrivilegedAction<T> action);
複製程式碼

而它引數的結構如下:

public interface PrivilegedAction<T> {
    T run();
}
複製程式碼

回到正題,boolean tryAdvance(Consumer<? super Integer> action) action)這個方法的具體流程是先判斷傳入的Lambda表示式是否為原始型別特化型別,如果是則直接呼叫boolean tryAdvance(IntConsumer action)方法。否則檢測org.openjdk.java.util.stream.tripwire是否開啟,如果開啟則生成日誌警告:{0} calling Spliterator.OfInt.tryAdvance((IntConsumer) action::accept),否則將Lambda強制轉化為原始型別特化型別並使用::建立方法引用。

總結

Spliterator作為java1.8新加入的對資料進行操作的類,它完全利用了函數語言程式設計的便利性,將資料的具體操作分離出去,大大提高了靈活性。同時採用類似於分支-合併的結構,為資料操作提供了並行的可能,提升了執行速度。和Iterator相比,Spliterator並不能直接將資料傳遞給我們。它只會執行我們給定的操作,同時它的遍歷是在其內部進行,不需要我們再去編寫for迴圈。

相關文章