从Presto堆栈讲解含有lambda表达式堆栈分析方法

关于labmda表达式

关于lambda表达式,labmda expressions in java 8 这里有比较简单的解释:

在java 里面,如果一个接口只有一个方法,那么这个接口就叫做function interface; 当然,我们可以按照传统方式去定义一个新的class来实现这个接口,但是,在有了lambda以后,我们也可以写一个lambda来完成。labmda表达式其实就是function interface的一种特殊的实现方式, 我们可以简单理解为,一个function interface的匿名实现类。所以,我们如果需要使用labmda表达式,就一定需要预先定义对应的function interface,然后,在需要的时候,定义对应的 labmda表达式,作为对这个interface的实现;

labmda表达式其实就是一个function interface的实现类,但是这个实现类是没有名字的,可以认为是一个function interface的匿名实现类;

我们遇到的最常见的function interface,就是Runnable接口,所以,为了使用Runnable,我们既可以按照传统方式去定义一个实现了Runnable接口的类:

public class MyRunnable implements Runnable{
    @Override
    public void run()
    {
    }
}

同时,也可以使用lambda表达式来实现:

Runnable myRunnable = () -> {
}

lambda 表达式带来编码上的便利

有了lambda表达式,我们在书写程序的时候变得更加方便快捷,带来了巨大的自由。

比如,我们在一个方法里面正在开心地写着一段逻辑,忽然发现我们对于某一个简单接口的实现类还没有写,这时候,如果不使用labmda表达式,我们需要去new一个class,让这个class实现该接口;如果这个接口有多个不同的实现,那么我们就需要新建对应的多个class,此时,我们可能会因为这个class应该叫什么名字,以及这个class应该放在哪个package下面而焦虑;

但是,有了lambda表达式以后,我们可以随手就把这个接口给实现了,不用想大多数情况下,我们可以在一个方法里面去实现我们需要的labmda表达式:

public void addQueryInfoStateChangeListener(StateChangeListener<QueryInfo> stateChangeListener)
{
 AtomicBoolean done = new AtomicBoolean();
 StateChangeListener<Optional<QueryInfo>> fireOnceStateChangeListener = finalQueryInfo ->     {
         if (finalQueryInfo.isPresent() && done.compareAndSet(false, true)) {
             stateChangeListener.stateChanged(finalQueryInfo.get());
         }
 };
 finalQueryInfo.addStateChangeListener(fireOnceStateChangeListener);
 fireOnceStateChangeListener.stateChanged(finalQueryInfo.get());
}

这里的StateChangeListener就是一个function interface,只有一个方法定义:

public interface StateChangeListener<T>
{
    void stateChanged(T newState);
}

这里很随意地就在addQueryInforStateChangeListener()中实现了接口StateChangeListener, 而且是按需实现,这个实现逻辑由于写在方法addQueryInforStateChangeListener()中,是因为这段实现只在方法addQueryInforStateChangeListener()中会发生,对于不关心这段实现的其它代码来说,这段实现完全隐藏;

我们也必须看到,lambda表达式和接口实现类虽然功能上相同,但是并不是说二者完全可以相互取代,因为他们的试用场景是不同的:一般情况下,如果这个接口的实现需要被很多业务共享,那么我们当然使用类定义的方式来实现接口,但是,如果这段接口的实现逻辑只是在当前场景下使用,那么我们完全没必要定义一个class暴露给所有人,只需要在当前位置立刻实现该接口就行,这个实现只在当前作用域(当前类、当前方法)中可见,其他人甚至不知道我们在这里还实现了这个接口;

lambda和非lambda表达式在堆栈信息上的差异

lambda表达式带来了coding上的巨大遍历,但是,也带来了代码和异常堆栈的可读性的降低,这甚至让很多人在coding的时候尽量不适用lambda表达式。

在没有正则表达式之前,我们看到一个完整的异常堆栈以后,可以有一下几个特点:

  1. 堆栈中的每一行,都是简单的类.方法的表达方式,一行堆栈不会出现多个方法,理解起来较直观
  2. 堆栈的上一行和堆栈的下一行存在明显的调用和被调用关系,读代码的时候从接口方法找到实现方法很容易
  3. 堆栈的每一行最后的行号,直接表示了当前堆栈的调用入口位置

比如,以下是一个没有lambda表达式的系统堆栈:

java.io.IOException: Wait for ZKClient creation timed out
at org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore$ZKAction.runWithCheck(ZKRMStateStore.java:1119)
at org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore$ZKAction.runWithRetries(ZKRMStateStore.java:1155)
at org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore.doStoreMultiWithRetries(ZKRMStateStore.java:947)

可以看到,堆栈信息非常明确,一行堆栈一个方法。如果我们对系统比较熟悉,我们甚至可以不看代码,直接从堆栈信息(类和方法的米那个字)上直观看到异常发生的时候的调用逻辑;

但是,如果堆栈信息中出现了lambda表达式,情况将不再那么简单,我们的堆栈信息变成了这样:

com.facebook.presto.event.query.QueryMonitor.queryCompletedEvent(QueryMonitor.java:282)
com.facebook.presto.execution.SqlQueryManager.lambda$createQueryInternal$4(SqlQueryManager.java:502)
com.facebook.presto.execution.QueryStateMachine.lambda$addQueryInfoStateChangeListener$10(QueryStateMachine.java:865)
com.facebook.presto.execution.StateMachine.lambda$fireStateChanged$0(StateMachine.java:230)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
java.lang.Thread.run(Thread.java:748)

在代码理解和通过堆栈对代码进行追踪上,lambda的出现让代码阅读和错误追踪变得困难很多,这主要是因为:

  1. labmda表达式堆栈的每一行,会出现以lambda开头的一个或者多个方法的连续嵌套,堆栈的形式变得难以理解
  2. 带有lambda表达式的堆栈已经无法给我们对应的调用关系,因为堆栈中的方法很可能与调用栈完全无关,只代表lambda表达式的定义位置
  3. 带有lambda表达式的堆栈中的行号,已经不代表执行流的位置,只代表lambda表达式的定义位置

下一结,关于lambda表达式的debug方式中,我们将具体来讲;

lambda表达式的debug方式

  1. 堆栈的每一行最后的行号,是这个lambda表达式中的某一个语句所在的那一行,但是这一行只是对应了这个lambda表达式的定义, 但是,调用堆栈并不是从这一行上方顺序执行下来的:

    例如,这一行堆栈信息:

    com.facebook.presto.execution.QueryStateMachine.lambda$addQueryInfoStateChangeListener$10(QueryStateMachine.java:865)
    

    它对应的代码位置是:

    public void addQueryInfoStateChangeListener(StateChangeListener<QueryInfo> stateChangeListener)
           {
               AtomicBoolean done = new AtomicBoolean();
     #863          StateChangeListener<Optional<QueryInfo>> fireOnceStateChangeListener = finalQueryInfo -> {
     #864              if (finalQueryInfo.isPresent() && done.compareAndSet(false, true)) {
     #865                  stateChangeListener.stateChanged(finalQueryInfo.get());
     #863              }
               };
               finalQueryInfo.addStateChangeListener(fireOnceStateChangeListener);
               fireOnceStateChangeListener.stateChanged(finalQueryInfo.get());
           }
    

    我们看到调用位置是第865行,虽然这一行代码位于方法addQueryInfoStateChangeListener中,但是方法addQueryInfoStateChangeListener只是用来对这个labmda进行定义,这个定义的执行很可能发生在系统启动的时候(可能系统一年前就启动过了),所以,此时的堆栈,跟addQueryInfoStateChangeListener没有任何关系了,865行代码发生时候的上一行代码应该是864,因为864也是这个labmda表达式,是顺序执行的,但是863862,也许是在一年以前系统刚启动的时候调用的;

  2. lambda表达式形成的堆栈信息中的类和方法,很可能与这个堆栈发生的运行时位置完全无关

    比如上文堆栈中:

    com.facebook.presto.execution.SqlQueryManager.lambda$createQueryInterna$l4(SqlQueryManager.java:502)

    最后一个数组 $14是这个labmda表达式的运行时的实例,也就是这个lambda表达式对应的function iinterface 的实现类的一个instance,由于是匿名实现,所以没有class 的名字。

    $14前面的方法SqlQueryManager这个类以及 createQueryInterna方法其实都与当前的调用逻辑没有任何关系,只代表了这个labmda表达式的定义位置。为了看清这一行堆栈到底发生什么,我们只能从createQueryInterna找到这个lambda表达式的定义:

    #404  private void createQueryInternal(QueryId queryId, SessionContext sessionContext, String       query)
    #405   {
    #406       // 代码略
    #407       QueryInfo queryInfo = queryExecution.getQueryInfo();
    #408       queryMonitor.queryCreatedEvent(queryInfo);
    #409       // 定义了lambda表达式
    #500       queryExecution.addFinalQueryInfoListener(finalQueryInfo -> {
    #501           try {
    #501               QueryInfo info = queryExecution.getQueryInfo();
    #501               stats.queryFinished(info);
    #502               queryMonitor.queryCompletedEvent(info);
    #503           }
    #504          finally {
    #505              // execution MUST be added to the expiration queue or there will be a leak
    #506               expirationQueue.add(queryExecution);
               }
           });
       }
    
  3. 由于labmda是接口的匿名实现,所以我们已经无法通过堆栈直接得出方法之间的调用关系,这时的调用与被调用关系必须结合源代码反推得出

    比如这两行堆栈信息:

    com.facebook.presto.execution.QueryStateMachine.lambda$addQueryInfoStateChangeListener$10(QueryStateMachine.java:865)
    com.facebook.presto.execution.StateMachine.lambda$fireStateChanged$0(StateMachine.java:222)
    

    从堆栈中,我们看到貌似调用关系是StateMachine.fireStateChanged() -> QueryStateMachine.addQueryInfoStateChangeListener,但是实际上调用关系跟这两个方法没有任何关系,这两个方法只是lambda表达式的定义位置。

    我们看看

    com.facebook.presto.execution.StateMachine.lambda$fireStateChanged$0(StateMachine.java:222)

    这一行堆栈对应的代码:

         private void fireStateChanged(T newState, FutureStateChange<T> futureStateChange, List<StateChangeListener<T>> stateChangeListeners)
         {
             checkState(!Thread.holdsLock(lock), "Can not fire state change event while holding the lock");
             requireNonNull(newState, "newState is null");
    #220     for (StateChangeListener<T> stateChangeListener : stateChangeListeners) {
    #221      try {
    #222          stateChangeListener.stateChanged(newState);
    #223      }
             catch (Throwable e) {
                 log.error(e, "Error notifying state change listener for %s", name);
             }
          }
         }
    

    显然,堆栈信息中的222行代码stateChangeListener.stateChanged(newState);是真正的堆栈调用位置,然后,我们需要查找stateChangeListener.stateChanged(newState);实际的代码位置,这时候就变得非常庞杂了,因为StateChangeListener这个接口通过lambda进行的实现可能出现在任何地方;

    对于堆栈信息,我们往上追追溯,看到当前异常发生的时候的这个lambda表达式的定义的位置是com.facebook.presto.execution.QueryStateMachine.lambda$addQueryInfoStateChangeListener$10(QueryStateMachine.java:865),我们对照代码找到QueryStateMachine.java:865:

    public void addQueryInfoStateChangeListener(StateChangeListener<QueryInfo> stateChangeListener)
             {
                 AtomicBoolean done = new AtomicBoolean();
                 StateChangeListener<Optional<QueryInfo>> fireOnceStateChangeListener = finalQueryInfo -> {
       #864              if (finalQueryInfo.isPresent() && done.compareAndSet(false, true)) {
       #865                  stateChangeListener.stateChanged(finalQueryInfo.get());
       #863              }
                 };
                 finalQueryInfo.addStateChangeListener(fireOnceStateChangeListener);
                 fireOnceStateChangeListener.stateChanged(finalQueryInfo.get());
             }
    

​ 可以看到,QueryStateMachine.java:865的确定义了一个StateChangeListener

总结

lambda表达式的出现给我们的编程带来了极大遍历,这种使用时再实现的自由编程思想现在已经变得越来越广泛,其封装性和细节隐藏也更强大;但是同时也降低了代码和堆栈可读性。在阅读含有lambda表达式的堆栈的时候,我们放弃阅读非lambda表达式堆栈的直观思维,将lambda表达式的定义和调用彻底区别开,才能准确理解代码;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值