idea使用帮助文档11(调试)

教程:Java调试深度潜水 

概述 

调试是任何开发人员库中最强大的工具之一。它为我们提供了对程序运行方式的独特见解,使我们能够更深入地了解我们调试的代码段。它允许我们跟踪正在运行的代码并检查状态和执行流程。作为其中的一部分,它给了我们顺序流动的幻觉。这非常直观且功能强大,但也可能会产生误导,因为大多数现代应用程序都是多线程的。

“调试”表明我们处理错误,但这实际上是用词不当。即使代码没有问题,我们从调试中获得的信息也很有用。发现错误恰好是我们从调试会话中获得的知识的一个非常常见的用例。

IntelliJ IDEA调试器提供丰富的体验,帮助我们轻松调试从最简单的代码到复杂的多线程应用程序。

在我们开始之前,请注意:调试是一个非常强大的工具,但它确实带来了成本。调试过程是运行时的一部分,因此会影响它。表达式的每个评估都使用调试应用程序的相同内存进行,并且可以修改并可能破坏状态。在本教程中,请记住调试是一种侵入式方法,可能会影响调试应用程序的结果。我们将探索一些方法来尽量减少其影响,有时甚至可以利用它。调试代码与运行代码相比,执行时间也大不相同。最小的调试跟踪开销本身可能已足以改变事件的时间,从而改变应用程序的行为。一切 断点 或日志是一个可能的同步点,并且步进显然会显着改变时序。正如我们将要看到的那样,这在多线程环境中成为一个关键问题,有时候再生错误取决于非常特定的事件序列。

最后要记住的是调试不能代替理解代码。实际上,从调试会话中学习的唯一方法是不断地将调试器向我们展示的信息与我们对代码的期望以及我们认为“应该”表现的方式进行比较。在开始调试会话之前,我们必须了解我们正在尝试通过它实现的目标。如果我们正在寻找一个bug,我们需要大致知道什么是不正确的,即与预期的行为或状态有什么不同。在大多数情况下,我们也会对事情出错的原因做一些初步的假设。这将决定我们的调试会话应该如何进行。调试时,我们必须始终将这些信息与我们的期望进行比较, 
这是调试非常有效的地方。 
这是我们学习的地方。

在本教程中,我们尝试深入研究调试技术,并假设您已熟悉基本概念,例如:

 

调试没有调试标志编译的代码 

无调试标志编译的代码无法调试。无法进入此代码。当调试器在调试会话期间遇到此类代码时,它将跳过该部分代码。

线断点也无法定义和命中。但是,这是 方法断点 可以保存我们的地方,因为我们仍然可以在IntelliJ IDEA中定义一个断点,在进入或退出特定方法之前停止,即使方法本身是在没有调试标志的情况下编译的。

查看状态时,由于无法检查方法中的实际变量,我们将看到警告消息。

变量调试信息不​​可用

没有源代码的调试 

如果我们没有特定代码的源代码,IntelliJ IDEA仍然会反编译该类并在反编译源中显示我们的步骤。这非常有用,但请注意,生成的反编译类可能看起来与原始类不同,如果行不匹配,则反编译代码中的调试可能会令人困惑。始终尝试获取要插入的类的源代码。

检测意外状态或流量 

本节介绍了如果我们知道事情已经出错的地方,但不知道原因。

探索调用帧 

甲 行断点 应足够用于检测一个意想不到的呼叫或呼叫突发参数值的方法背后的原因多数情况下。如果我们不确定它的调用位置,我们可以将断点放在方法中。当VM挂起时,单击前面的调用帧以查看调用堆栈并检查每个范围中的状态以查看我们如何到达此处。

呼叫帧

丢帧 

如果我们走得太远并且想要返回堆栈然后重新执行代码,我们可以使用 Drop Frame 功能。这是一个有用的功能,但也有潜在危险:我们必须意识到重新执行代码将执行相同的指令两次,如果这些指令修改状态,我们可能最终处于损坏状态,当然在不会出现的情况下在相同条件下正常运行。为了使Drop Frame的影响 明显,请考虑以下简单程序:

 

public class DropFrameDemo
{
    private static int state = 0;

    public static void main(final String[] args)
    {
        modifyStateBasedOnParameter(state);
        modifyStateBasedOnStaticField();
    }

    // dropping frame within this method,
    // and executing again will print state = 2
    private static void modifyStateBasedOnStaticField()
    {
        state++;
        System.out.println("state = " + state);
    }

    // dropping frame from within this method,
    // and executing again will print state = 1
    private static void modifyStateBasedOnParameter(final int parameter)
    {
        state = parameter + 1;
        System.out.println("state = " + state);
    }
}

内部破坏 modifyStateBasedOnParameter() 不会影响状态,因为IntelliJ IDEA会记住传入该帧的参数值,并且不会重新计算这些参数值。但是,打破内部 modifyStateBasedOnStaticField() 将使该 state 字段等于'2'。在正常运行的情况下不可能的值 main()

通过方法检测意外流量 

在旧版本的IntelliJ IDEA中,方法断点会显着降低执行速度。 
从2017.1版开始,方法断点实际上是由行断点模拟的,因此速度也一样快。

在有问题的方法中定义行断点的替代方法是定义 方法断点。此类断点未附加到源代码行号,而是附加到对方法的调用的进入和退出。它在两个主要情况下特别有用:

  • 当一个方法由一个接口定义时,我们想要在它的所有实现中断点。

  • 当我们没有源代码,只有反编译版本时,我们仍然想检查方法调用的细节,而编译类和反编译源代码之间的行号没有任何混淆的差异。

 

检测意外的对象状态 

字段监视点是一种断点,可显着降低执行速度,应谨慎使用,尤其是在时序更改可能影响方案的多线程应用程序中。

有时难以弄清楚导致场到达某种意外状态的确切流量。在这些特殊情况下,我们可以使用断点,当程序读取或写入特定字段时,该断点将被命中。见Field Watchpoint

检测抛出的意外异常 

虽然不是严格的调试功能,但是当我们想要调查抛出异常的原因时,我们可以 分析异常堆栈跟踪 并快速到达生成该异常的代码行。从那里,线断点和 步进的组合通常足以找出问题所在。

分析Stacktrace

但有时,异常包含在另一个异常中,或者被catch块捕获并吞没。我们所看到的只是它的副作用(可能是一个日志),而不是它的堆栈跟踪。为此,我们可以使用异常断点

调试异步流程 

反应式编程越来越受欢迎,在许多框架和库的帮助下,开发人员正在编写更多的异步代码。 
异步应用程序中的流程是调试工具和使用它们的开发人员面临的主要挑战。执行在帧之间跳转,使得理解和遵循代码变得更加困难。 
向前迈进是更容易的一点。我们可以在代码中的不同点插入断点,无论执行的线程如何,都可以看到从一个代码片段到下一个代码片段的进度。 
这样做时,要注意断点是仅挂起一个线程还是整个应用程序。该决定基于调试会话的目标。如果要检查所有线程的状态并查看哪些线程已经进展以及哪些线程可能不必要地等待,您可以在此时冻结整个系统并查看所有线程的调用帧和堆栈跟踪。
如果您正在调试特定操作,则可以暂停一个线程并让系统的其余部分继续工作。

异步Stacktraces  

当我们想要从代码中的特定点回顾并理解我们如何到达时,异步调试的真正痛苦就开始了。考虑下面的异步代码示例(使用JDK的CompletableFutures):

 

private void asyncExample() throws InterruptedException, ExecutionException
{
    final CompletableFuture<String> future = supplyAsync(() -> "F").thenApplyAsync(this::append_oo);
    System.out.println(future.get());
}

private String append_oo(String str)
{
    return str +"oo";
}

当我们在方法内部停止时,append_oo我们可以看到堆栈跟踪为我们提供的信息非常少。 
具体来说,我们无法看到未来应用于它上面,也无法看到asyncExample我们开始这一切的方法。 

ij调试异步堆栈跟踪

 

在异步上下文中,堆栈跟踪只会向我们展示一个非常有限的图片,我们真正需要的是线程之间的信息流或所有线程的组合堆栈跟踪,这些都是我们到达这一点(也称为因果链) 。

IntelliJ IDEA提供了一种查看这些异步堆栈跟踪的方法。 
在调试会话中,IntelliJ IDEA将捕获堆栈跟踪,并在以后查看异步流的下一部分的堆栈跟踪时显示它们: 

ij调试异步因果关系链


当我们过滤外部库时甚至更清楚: 

ij调试异步因果链过滤


要捕获的堆栈跟踪以及插入它们的点需要在异步堆栈跟踪下的调试器首选项中进行配置。 
IntelliJ IDEA需要知道要显示在我们需要捕获的堆栈跟踪顶部的类名和方法。在我们的例子中java.util.concurrent.CompletableFuture thenApplyAsync。调试器还需要知道我们想要插入捕获的堆栈跟踪的另一个堆栈跟踪中的位置。在我们的例子中它是 java.util.concurrent.CompletableFuture$AsyncApply exec。 
为了匹配两个堆栈跟踪,我们还需要两个键 - 每个上下文一个键,当一个堆栈跟踪确实是我们的异步链中的下一个逻辑步骤时,它将指向相同的值。在我们的示例中,我们可以使用的键表达式是param_0(参数0 - 我们传递给thenApplyAsync的方法引用)。它将匹配fn- 内部的变量,java.util.concurrent.CompletableFuture$AsyncApply 用于保存第二个调用帧中的函数。

ij调试异步捕获


这应该在调试器设置中配置一次,并且将提供常用异步框架的配置,例如 此处使用的CompletableFuture。 

 

调试多线程应用程序 

多线程应用程序是调试的最大挑战。这些应用程序不具有确定性,而且难以控制。我们从步入调试会话中获得的顺序流的错觉 也无济于事,可能会产生误导。 
在调查可能是并发错误的问题时,我们需要尝试更少步骤并 更多地调整我们的断点。这是因为很多并发错误都依赖于不同线程之间的特定交互,而侵入式调试会话会干扰它。我们将展示如何使用各种 Breakpoint属性 允许我们将干扰限制在最低限度。另一个重要的主题是 在应用程序中控制和 切换不同的线程。我们将通过调试不同并发错误的一些示例来演示IntelliJ IDEA的功能如何帮助解决这个问题。

控制断点 

IntelliJ IDEA 调试器属性 允许我们控制 触发断点时采取的操作 。他们中的一些人定义了一个行动,而其他人则在那里增加了是否采取行动的进一步条件。这种对断点的精细控制对于并发错误至关重要,因为大多数只有在线程以非常特定的方式进行交互时才会被重现。断点的任何干扰都可能阻止我们重现这个bug。

断点动作

确定断点操作取决于我们希望在调试会话中实现的目标。

如果我们可以在代码中定义条件或点,我们可以从查看整个系统状态获得更多信息,我们应该暂停整个VM。

有时,最好只挂起一个线程而不是整个VM。当应用程序是较大系统的一部分并且挂起VM将导致等待服务的消息溢出或请求超时最终破坏整个系统时,尤其如此。当我们有许多工作线程时,最好保持几乎所有工作,并专注于我们感兴趣的一个线程。

当我们处理并发错误时,任何暂停执行都可能阻止我们重现错误。我们可以选择做断点不停止什么,只是 记录 无论是消息或特定表达到控制台的值,然后检查日志。当我们有一个关于我们究竟在寻找什么的强有力的理论时,这很有效。

根据条件限制断点

除了方便之外,断点条件还允许我们最小化调试会话的侵入性。它们允许我们将断点操作限制为我们认为绝对必要的操作。

条件本身具有开销,并且每次遇到断点时都会对其进行评估。

条件表达式 是使用最广泛的条件。它们允许我们仅在应用程序达到特定状态时触发断点。理想的情况是,我们可以定义一个表达式来捕捉事情开始出错的确切点。

传递计数 在多次运行的代码中非常有用,无论是事件处理程序还是循环,我们所关注的有趣场景仅在特定次数的传递后显示。

我们在代码被多次命中时使用它,但只有第一种情况很有趣。该删除一旦撞上选择是在两种情况下尤其有用:

  • 当断点操作是记录而不是挂起时,这意味着我们无法在命中之后删除或禁用断点。

  • 当代码由许多线程执行时,我们只想暂停其中一个。

 

一个非常有用的功能。它的明显用途是作为一个过滤器来触发断点,在这种场景中我们只是在访问一个方法后感兴趣,或者只在达到另一个状态后才对代码中的特定状态感兴趣。但是,除此之外,我们可以使用它来重现特定的并发问题,因为它可以帮助我们挂起线程并控制哪个线程到达代码中的哪个特定行以及以哪个顺序。

允许我们按类或特定实例过滤触发。

调试长时间运行的场景 

方法断点Field Watchpoint大大减慢了代码执行速度。当执行相同的代码很多次,甚至是 条件断点 减慢处理速度以使其显而易见。这是一个真正的问题,因为处理数百万个事件的事件处理程序的场景相当普遍(考虑重放日志文件或处理大量生产日志文件)并评估该事件处理代码中的断点条件可能会使系统变慢为无法使用的状态。为了克服这个问题,假设我们可以修改运行代码,我们可以通过使用一个小技巧来提高速度,我们称之为“代码中的断点”。当我们调试数百万个事件的处理时,这个技巧非常有用,其中只有一个事件导致问题而且我们事先不知道哪个是有问题的事件,并且可以节省我们很多等待触发条件断点的事件。最快的代码是由JVM编译和优化的执行代码。我们想要使用这个事实,因此,我们不是在断点上写条件,而是以我们稍后可以操作的方式将它引入到我们执行的代码中。然后,我们在没有任何断点的情况下进行调试,从而以最快的方式运行调试会话,并在代码运行时引入断点,只有当我们真正知道我们会发现它时。

代码中的断点 

  1. 我们用条件为代码引入了一个循环。这意味着只有在感兴趣的状态发生时我们才进入循环。然后我们将某些内容打印到控制台,以便我们知道代码何时进入循环。因为循环不会改变任何状态,所以一旦我们进入循环,我们将保持在其中。
    while (bugCondition(msg))
    {
        System.out.println("gotcha!");
        try
        {
            Thread.sleep(1000);
        }
        catch (InterruptedException e)
        {
            //ignore
        }
    }

    这里的睡眠只是为了避免用“陷阱”轰炸控制台!消息。

  2. 此时我们启动调试会话,坐下等待“陷阱!” 出现。控制台将告诉我们“击中”“断点”。

    控制台显示断点命中

  3. 当“陷阱!” 确实出现了,我们在循环中引入了一个实线断点。断点将被命中并暂停VM或线程。现在我们可以检查事件及其状态。如果检查不够,最后要做的是让我们的代码退出循环。有两种选择。

    • 我们可以利用Evaluate Expression侵入性来评估将实际将循环条件修改为false的代码片段。如果我们使用字段或变量作为循环的条件,这很容易完成,因为我们可以修改它的值。

      boolean enterLoop = bugCondition(msg);
      while (enterLoop)
      {
          System.out.println("gotcha!");
          try
          {
              Thread.sleep(1_000);
          }
          catch (InterruptedException e)
          {
              //ignore
          }
      }

      使用Evaluate Expression停止循环

    • 我们可以使用调试会话的另一个功能HotSwap退出循环 。这允许我们在调试期间修改正在运行的代码,编译它然后IntelliJ IDEA  使用新版本热调试调试的类。我们需要做的就是将循环条件更改为“false”。默认情况下,IntelliJ IDEA将检测到某个类是否具有新版本,并将询问我们是否使用新版本重新加载该类。

      重新加载课程?

      加载新版本后,新的循环条件将使代码退出循环,我们可以从该点继续调试。您可以在循环之后放置另一个断点以再次暂停执行,或者只是逐步执行'false'循环条件。

请记住在调试后删除此代码。您还应该对真实功能进行失败的测试,以提醒您永远不要错误地提交它。

寻找竞争条件 

竞争条件是多线程应用程序中的常见问题。多个线程访问并修改相同的状态,可能会破坏它或导致不希望的流量。竞争条件可能是一个非常微妙的错误,通常难以重现。这是因为它只在线程以非常特定的顺序执行代码时才会发生。其他执行订单看起来很好,不会引起任何问题。

在线程中查找竞争条件时,我们的调试运行必须以尽可能少的干扰开始,以免干扰执行顺序。一旦我们获得了一些信息或假设执行顺序会导致错误,我们也可以使用调试功能使用断点之间依赖关系来重现它 。

检测竞争条件导致状态损坏 

有时竞争条件仅在系统的每十次或一百次运行中发生一次。如果我们怀疑我们的多线程代码中存在竞争条件,我们必须始终确保调试会话的侵入性不会使问题不可重现。例如,这里我们创建了一个发布者和订阅者系统,但是我们所有的订阅者共享一个原始(而不是线程安全)计数器来计算消费消息的总数。

 

private class Subscriber implements Runnable
{
    @Override
    public void run()
    {
        while (true)
        {
            String msg = messageQueue.poll();
            if (msg != null)
            {
                if (msg.equals(STOP))
                {
                    break;
                }
                else
                {
                    // race condition right here!
                    counter++;
                }
            }
        }
    }
}

一旦我们确定可以在调试模式下重现问题,我们尝试使用日志记录设置 断点 而不是暂停程序执行。在这里,我们从所有线程登录到同一控制台的事实可能会以一种“解决”错误的方式“同步”线程。我们需要确保即使现在可能需要更多尝试,我们仍然可以重现它。记录可疑状态可以缩小我们的选项范围,并允许我们看到问题不在于对方法的调用次数,而在于计数器字段。

避免调试器开销 

在大多数机器上,竞争条件(例如我们之前示例中的竞争条件)将变成“微妙”竞争条件。“微妙”意味着对运行时环境的任何修改或更改都可以“修复”它。请记住,错误的起源是推进原始计数器不是原子操作的事实。

 

// race condition right here!
counter++;

为了重现这个错误,我们需要两个线程,它们都读取相同的值:“第二个”线程必须在“第一个”更新它之前读取该值并刷新其CPU缓存。在正常运行期间很容易在多核机器上创建,但在调试会话中几乎不可能重现。

通过断点在同一点上记录同步线程,因为它们都需要写入同一个日志。这也会刷新所有线程的CPU缓存,因为写入日志是原子的。简而言之,它阻止我们再现错误。暂停VM或线程也无法帮助我们,因为我们无法将两条指令(读取计数器值并递增)分开来中断它们。在这一点上,我们需要做出一些假设然后证明或反驳它们。由于我们不能使用任何断点,我们唯一的希望是我们可以更改实际执行的代码并引入将被编译的新代码,因此会减少干扰。

这是非常不得已的选择。一个很好的模式来帮助我们这里是一个跟踪缓冲区。

跟踪缓冲区

我们可以引入一个内部缓冲区并将有趣的值存储在这个缓冲区中。将其视为本地化,非常高效的内存日志。我们必须确保:

  • 我们每个线程都有一个缓冲区,这些缓冲区是隔离的,所以这不会引入新的并发问题。

  • 因为缓冲区是每个线程,所以它不需要是线程安全的,并且不能是线程安全的。这是因为我们希望避免引入任何同步点。

  • 我们插入的值不能引用可以更改的实际状态,而是复制或记录消息。

  • 引入的代码尽可能小,以最小化其对运行代码的影响。

  • 我们仅在执行结束后打印或记录缓冲区的内容,以避免使日志记录操作成为线程之间的同步机制。另一种选择是仅将值存储在跟踪缓冲区中,然后在代码的关键部分执行完毕后通过设置断点来检查其内容。

 

private class Subscriber implements Runnable
{
    private int index = 0;
    private final int[] traceBuffer = new int[NUMBER_OF_SUBSCRIBERS_AND_PUBLISHERS * 100];

    @Override
    public void run()
    {
        while (true)
        {
            String msg = messageQueue.poll();
            if (msg != null)
            {
                if (msg.equals(STOP))
                {
                    break;
                }
                else
                {
                    traceBuffer[index++] = counter;
                    // race condition right here!
                    counter++;
                }
            }
        }
    }
}

例如,这里我们int array为所有消息引入了一个足够大的原语,为了证明我们对计数器中的错误的怀疑,我们在推进之前只存储计数器值。是的,它可能不是计数器提前确切的值,但如果多个线程报告相同的计数器值,它将证明我们的假设。完成所有事件后,我们可以检查跟踪缓冲区并找到重复项。

检测竞争条件导致意外的流量控制 

 

private class Subscriber implements Runnable
{
    @Override
    public void run()
    {
        String msg;
        while (true)
        {
            msg = messageQueue.poll();
            if (msg != null)
            {
                if (msg.equals(STOP))
                {
                    break;
                }
                // else do something
            }
        }
        // Will NOT work with multiple subscribers, as main thread will
        // wake up when the first subscriber is done.
        // Using a CountDownLatch here is a much better approach.
        synchronized (messageQueue)
        {
            messageQueue.notify();
        }
    }
}

在这个例子中,我们有另一个竞争条件,但争用的共享状态不是直接可见的,只是由一个看似错误的流控制推断:第一个订阅者将唤醒主线程,即使第二个线程是仍在处理消息。在这种情况下,我们无法在“跟踪缓冲区”中检查或打印等待线程,但是当我们挂起整个应用程序时,我们可以检查各种线程的位置。

  1. 首先,我们可以在唤醒后暂停主线程。我们可以看到有时其中一个订户线程仍然标记为正在运行。这使我们可以假设问题的根源在于该notify()方法过早被调用。

  2. 我们只能暂停其中一个订户线程。这将导致另一个线程通知主线程。这将向我们证明问题可能发生在任何订户中并且在其逻辑中。

  3. 可以肯定的是,我们可以提前暂停整个VM,就在订户通知主线程之前。然后,我们可以检查两个订户线程的状态,并证明其中一个仍在轮询,而另一个已经完成并且即将通知主线程已完成。

    在通知主线程之前暂停应用程序

    在这个屏幕截图中,我们可以看到检查线程都标记为'RUNNING',这意味着当第一个即将通知主线程时它已经完成(在同步块内),另一个仍然可以处理消息。

  4. 为了毫无疑问地证明我们的假设,我们还可以在订阅者的轮询循环中放置一个断点。在我们通知主线程之前,我们使断点依赖于前一个断点。击中从属断点(如下所示)证明了我们的理论。

    订户内部的断点

检测死锁 

当两个线程发生冲突时会发生死锁,两者都会阻止彼此工作。一旦发生,通过查看所有线程的帧,很容易发现死锁。我们可以通过使用 线程转储来实现运行时可以使用此功能 。如果我们知道我们正在追逐死锁,那么运行模式甚至比调试更可取。这是因为我们不会以这种方式干涉执行,而快照将是应用程序的Java线程转储的输出。线程转储可以检测死锁并对它们发出警告。例如,在下面的转储中,我们可以看到进程在PublisherThread(它停留在第44行)和SubscriberThread(第78行)之间发现1个死锁。

线程转储

  1. 在这个例子中,我们可以看到两个线程都在等待锁定,这意味着另一个线程没有释放这些锁。我们还可以看到两者都在等待不同的锁,因为同步器ID是不同的。更有用的是顶部的死锁摘要告诉我们哪个线程持有每个锁。我们可以看到两个死锁的线程正在持有另一个线程试图获取的锁。
  2. 这应该已经为我们提供了有关死锁如何发生的大量信息。如果仍然不清楚我们的代码如何达到死锁,那么我们可以在我们点击线程转储提供的行之前尝试使用断点进行调试。当我们有一个错误的理论时,我们可以尝试使用断点之间依赖关系来重现场景

  3. 我们现在可以 在其中一个线程上创建一个Suspend Thread断点,并使用另一个线程转储快照验证另一个线程是否到达其死锁位置。

    Publisher线程中的断点

    现在我们可以在其中一个线程陷入死锁之前检查状态。
  4. 另一种选择是在两个线程上放置挂起线程断点并在它们之间切换。检查的状态Publisher,并Subscriber 在这个例子告诉我们,造成僵局的混乱。

    出版商的断点

    订户中的断点

  5. 当我们检查我们的锁实例时,我们可以看到并发代码实际上是正确的,但是当我们将它们传递给两个对象时,我们混淆了读锁和写锁。查看上图中的锁实例ID。

  6. 实际上,当我们检查构造(我们注入这些锁的地方)时,我们可以看到错误:

    ThreadGroup threadGroup = new ThreadGroup("Demo");
    
    new Thread(threadGroup, new Subscriber(messageQueue, readLock, writeLock), "SubscriberThread").start();
    
    //passing locks in the wrong order will cause deadlock between publisher and subscriber
    new Thread(threadGroup, new Publisher(messageQueue, writeLock, readLock), "PublisherThread").start();

     

检测活锁 

活锁是线程未被阻止但仍无法取得进展的情况。从外部来看,活锁应该像死锁一样,但由于线程没有被阻塞,快照(线程转储)不会在任何死锁时提醒我们。

  1. 在开始调试之前尝试的一种策略是 多次重复 Thread Dump,然后比较各种线程的堆栈跟踪。这应该让我们清楚地看到代码中有问题的区域,在大多数情况下代码无法逃避。

    Livelock快照

  2. 如果我们仍然不确定,我们可以使用 大量的 Pass Count,我们假设只有在活锁情况下才能达到。

    使用传球计数

    我们还可以逐步执行代码并确切地确认重复执行的代码区域,但不会进展。
  3. 此时, 当我们进入活锁状态时(即,当应用程序达到阻止其逃避执行的代码块的状态时),我们可以使用条件表达式来捕获执行中的点。

    在这个例子中,我们假设STOP消息未能将我们从循环中断开,因此要么它从未被发送过,要么它没有被处理,所以我们将引入一个断点,其条件是查找STOP消息。

    使用条件来捕获活锁情况

  4. 我们的断点被击中,意味着STOP消息已发送但未处理。

    断点命中

  5. 我们介入,使用Evaluate Expression检查状态并找到错误。“有效”方法不认为STOP是有效的消息,并将我们置于此活锁场景中。

    使用Evaluate Expression来测试理论

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值