天天看点

【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;
    }
           

存了一个下一次必须调用求值方法的时间戳,然后每一次调用都比较一下看是否需要求值。存时间戳也是一个常见的实现周期性调用的策略。