天天看點

【Java】guava(六)函數式程式設計與惰性求值

舉個例子,比如我們的web伺服器應用,我們可能會寫一個類似攔截器一樣的子產品來提前把一些公共參數抽取出來,比如像token,userid,ip位址等等這樣的資訊,放入一個類似ThreadLocal的對象中,後面的controller如果想要用就可以直接拿。

方案一:及早求值,每一次都直接計算出最終結果,存放至ThreadLocal;

實作起來很直覺也很簡單,但是可能大多數請求在被後面的controller處理時,隻會使用其中的一個或幾個,甚至一個都不用,這樣的話,每一個都計算就會很浪費資源了。

方案二:惰性求值,在ThreadLocal處隻存儲求值方法,但是不執行求值過程,隻有在真正使用時才執行求值過程。這個方案比上面的好多了,起碼避免了大量的無用計算。但是可能有一個問題,如果我需要多次使用其中的變量,進行了多次調用,那麼就會計算多次,好像是重複計算。确實有這個問題,但是如果值在整個請求生命周期中不變,比如像userId,ip這種的,其實可以調用一次,然後存入一個變量,後面直接使用這個變量即可。

Guava的函數式程式設計Supplier對上述惰性求值有很好的支援,下面看下:

先看下guava的Supplier接口:

@GwtCompatible
@FunctionalInterface
public interface Supplier<T> extends java.util.function.Supplier<T> {
  /**
   * Retrieves an instance of the appropriate type. The returned object may or may not be a new
   * instance, depending on the implementation.
   *
   * @return an instance of the appropriate type
   */
  @CanIgnoreReturnValue
  @Override
  T get();
}
           

我們求值的方法需要被封在這個接口的get方法裡。

例子:

@Test
    public void testD() throws InterruptedException {
        Supplier<Integer> id1 = Suppliers.memoize(() -> fetch());
        Supplier<Integer> id2 = Suppliers.memoizeWithExpiration(() -> fetch(), 1, TimeUnit.SECONDS);
        System.out.println("****************");
        for (int i = 0 ; i < 10; i ++) {
            System.out.println(id1.get());
            System.out.println(id2.get());
            System.out.println("****************");
            Thread.sleep(1000);
        }
    }

    private int fetch(){
        return new Random().nextInt(10);
    }
           

****************

4

5

****************

4

9

****************

4

3

****************

4

3

****************

4

2

****************

4

8

****************

4

1

****************

4

****************

4

3

****************

4

4

****************

Process finished with exit code 0

将我們的求值方法封裝在一個Supplier接口内,可以直接new一個Supplier。這種方式是最原始的,每一次get都會調用一次求值。對于很簡單的求值計算,似乎這種已經足夠了。

但是guava為我們提供了另外兩種更進階的用法。

一個是memoize方法,内部會保證求值方法隻在第一次get時被調用,然後結果被存下來,後面的get直接傳回結果即可。

當然,如果求值過程需要更新怎麼辦?

guava提供了另一個memoizeWithExpiration方法, 會根據時間間隔适時調用求值方法更新本地緩存的結果。

上面的例子很清楚。

看下實作:

對于memoize,其内部方法是這樣實作的:

public T get() {
      // A 2-field variant of Double Checked Locking.
      if (!initialized) {
        synchronized (this) {
          if (!initialized) {
            T t = delegate.get();
            value = t;
            initialized = true;
            // Release the delegate to GC.
            delegate = null;
            return t;
          }
        }
      }
      return value;
    }
           

有點類似單例模式,使用了一個同步操作判定是否初始化了。

對于memoizeWithExpiration,其内部方法是這樣實作的:

public T get() {
      // Another variant of Double Checked Locking.
      //
      // We use two volatile reads. We could reduce this to one by
      // putting our fields into a holder class, but (at least on x86)
      // the extra memory consumption and indirection are more
      // expensive than the extra volatile reads.
      long nanos = expirationNanos;
      long now = Platform.systemNanoTime();
      if (nanos == 0 || now - nanos >= 0) {
        synchronized (this) {
          if (nanos == expirationNanos) { // recheck for lost race
            T t = delegate.get();
            value = t;
            nanos = now + durationNanos;
            // In the very unlikely event that nanos is 0, set it to 1;
            // no one will notice 1 ns of tardiness.
            expirationNanos = (nanos == 0) ? 1 : nanos;
            return t;
          }
        }
      }
      return value;
    }
           

存了一個下一次必須調用求值方法的時間戳,然後每一次調用都比較一下看是否需要求值。存時間戳也是一個常見的實作周期性調用的政策。