无限while循环内的Thread.sleep不会引发异常-为什么?

时间:2019-05-15 12:48:49

标签: java multithreading

我的问题是关于InterruptedException,它是从Thread.sleep方法抛出的。在使用ExecutorService时,我注意到一些我不理解的奇怪行为;这是我的意思:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

使用此代码,编译器不会给我任何错误或消息,即应该捕获InterruptedException中的Thread.sleep。但是,当我尝试更改循环条件并将“ true”替换为这样的变量时:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

编译器不断抱怨必须InterruptedException处理。有人可以向我解释为什么会发生这种情况,以及为什么如果将条件设置为true则编译器会忽略InterruptedException?

2 个答案:

答案 0 :(得分:56)

之所以这样做,是因为这些调用实际上是对两个不同的重载方法的调用,它们采用两种不同类型的参数,每种类型具有不同的异常处理规范:

  
      
  1. <T> Future<T> submit(Callable<T> task);
  2.   
  3. Future<?> submit(Runnable task);
  4.   

然后发生的是,编译器将问题的第一种情况下的lambda转换为Callable<?>功能接口(调用第一个重载方法);在您遇到问题的第二种情况下,将lambda转换为Runnable功能接口(因此调用了第二个重载方法)。

尽管两个功能接口都没有参数,但是Callable<?>返回一个值,并且它 throws Exception 非常重要!):

  
      
  1. 可致电:V call() throws Exception;
  2.   
  3. 可运行:public abstract void run();
  4.   

如果我们切换到将代码修整为相关部分的示例(以便轻松地研究好奇的位),那么我们可以编写与原始示例等效的代码:

    ExecutorService executor = Executors.newSingleThreadExecutor();

    // LAMBDA COMPILED INTO A 'Callable<?>'
    executor.submit(() -> {
        while (true)
            throw new Exception();
    });

    // LAMBDA COMPILED INTO A 'Runnable': EXCEPTIONS MUST BE HANDLED BY LAMBDA ITSELF!
    executor.submit(() -> {
        boolean value = true;
        while (value)
            throw new Exception();
    });

在这些示例中,可能更容易观察到第一个被转换为Callable<?>而第二个被转换为Runnable的原因是由于 compiler推论

在第一种情况下,编译器执行以下操作:

  1. 检测到lambda中的所有执行路径都声明抛出checked exceptions(从现在开始,我们将其称为'exception',这仅表示'checked exceptions' )。这包括调用任何声明抛出异常的方法,以及对throw new <CHECKED_EXCEPTION>()的显式调用。
  2. 正确推断lambda的 WHOLE 主体等效于声明抛出异常的代码块;当然,必须必须:处理或重新抛出。
  3. 由于lambda不处理异常,因此编译器默认情况下假定必须重新抛出这些异常。
  4. 安全地推断此lambda必须与引发异常的功能接口匹配。
  5. 由于Callable<?>是可用于重载方法的唯一匹配功能接口,因此它将选择它,将lambda转换为Callable<?>并创建对submit(Callable<?>)重载方法的调用引用。

在第二种情况下,编译器执行以下操作:

  1. 检测到lambda中可能存在请勿声明抛出异常的执行路径(取决于待评估逻辑)。
  2. 由于并非所有执行路径都声明了抛出异常,因此编译器得出结论,lambda的主体与声明抛出异常的代码块 NOT NECESSARILY 等效-编译器不在乎/注意是否该代码的某些部分确实声明了它们可以,无论整个主体是否这样做。
  3. 编译器将Callable<?>视为lambda的匹配功能接口,因为Callable 确实声明了抛出异常。 (a)
  4. 选择Runnable作为要转换为lambda的其余 fitting 功能接口,并创建对submit(Runnable)重载方法的调用引用。所有这一切都以委派给用户为代价,处理在{strong> MAY 处抛出的任何Exception的责任都发生在lambda主体的一部分内。

(a)除了使Callable<?>为a之外,编译器没有任何理由不默认不将所有lambda都转换为Callable<?>(并减轻内部复杂性)。比可用的可行替代方案(即Runnable)更多的限制性功能界面。这种行为符合的原则,即必须严格遵守。但与一个CAN'一样不受限制。

这是一个很好的问题-追逐它,我有很多乐趣,谢谢!

编辑(响应@罗马更正):

我的回答是正确的。编译器之所以决定将问题中的第一个lambda编译为Callable<T>,而将第二个lambda编译为Runnable,则是因为其隐式声明例外特征。以下是一个更简单的例子,可以说明这一点:

    // LAMBDA COMPILED INTO A 'Callable<?>'
    Executors.newSingleThreadExecutor().submit(() -> { throw new Exception(); });

    // LAMBDA COMPILED INTO A 'Runnable'
    Executors.newSingleThreadExecutor().submit(() -> {                        });

因此,我正确回答了最初提出的问题-结案了。

现在,从更一般的意义上讲,显然编译器会考虑lambda的 ALL SIGNATURE FEATURES (所有签名特征)来确定将任何lambda编译为(包括参数类型和return类型);但这不是问题所在;如前面的简单示例中清楚显示的那样。

对于示例,您基于整个参数:

    Executors.newSingleThreadExecutor().submit(() -> {
        while (true) {
            Thread.sleep(5000);
        }
    });

尽管您有索赔...

  

例如,以下构造将无法编译

     

编译器无法分辨它是Runnable还是Callable;可以是任何一个。

...此示例确实可以编译。编译器 CAN 告知lambda符合Callable<T>的签名。实际上,该示例与第一个问题相同,因此我不知道您在说什么。

最后,您随后包含的示例与问题中显示的示例不等效。您添加了return条语句;这改变了问题,因为它为lambda添加了原始问题中不存在的功能(即lambda的返回类型以区分将其编译为哪个功能接口)并且需要不同的答案;但是再次就没问题了;更糟的是,现在我在问题中看到您的新评论,要求对问题进行修改/编辑以适合您的答案。

答案 1 :(得分:1)

简短

ExecutorService同时具有submit(Callable)submit(Runnable)方法。

  1. 在第一种情况下(带有while (true)),submit(Callable)submit(Runnable)都匹配,因此编译器必须在它们之间进行选择
      之所以选择{li> submit(Callable)而不是submit(Runnable),是因为CallableRunnable更具体
    • Callablethrows Exception中有call(),因此没有必要在其中捕获异常
  2. 在第二种情况下(while (tasksObserving))仅submit(Runnable)匹配,因此编译器选择它
    • Runnable的{​​{1}}方法没有throws声明,因此,无法捕获run()方法内部的异常是编译错误。

完整故事

Java语言规范描述了在$15.2.2中的程序编译过程中如何选择方法:

  1. 确定可能适用的方法($15.12.2.1),该过程分三个阶段进行,以严格,宽松和可变的arity调用
  2. 从第一步中找到的方法中选择最具体的方法($15.12.2.5)。

让我们用OP提供的两个代码段中的2个run()方法来分析情况:

submit()

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

(其中ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> { while(tasksObserving) { //DO SOMETHING Thread.sleep(5000); } }); 不是最终变量)。

确定潜在的适用方法

首先,编译器必须确定可能适用的方法:$ 15.12.2.1

  

如果成员是具有固定性n的固定固定性方法,则方法调用的奇偶性等于n,并且对于所有i(1≤i≤n),方法调用的第i个参数为潜在兼容。

在同一部分中进一步

  

根据以下规则,表达式与目标类型可能兼容

     

如果满足以下所有条件,则lambda表达式(第15.27节)可能与功能接口类型(第9.8节)兼容:

     

目标类型的函数类型的奇偶性与lambda表达式的奇偶性相同。

     

如果目标类型的函数类型具有void返回值,则lambda主体可以是语句表达式(第14.8节)或与void兼容的块(第15.27.2节)。

     

如果目标类型的函数类型具有(非无效)返回类型,则lambda主体可以是表达式或值兼容的块(第15.27.2节)。

请注意,在这两种情况下,lambda都是块lambda。

我们还要注意,tasksObserving的返回类型为Runnable,因此与void 可能兼容 时,block lambda必须为 void-兼容块。同时,Runnable具有非无效的返回类型,因此要与Callable具有可能的兼容性,块lambda必须是值兼容的块< / em>。

$ 15.27.2定义 void-compatible-block value-compatible-block 是什么。

  

如果块中的每个return语句的格式均为Callable,则lambda主体是无效的。

     

如果一个lambda主体不能正常完成(第14.21节)并且该块中的每个return语句的形式为return;,则它是值兼容的。

让我们看看$ 14.21,有关return Expression;循环的段落:

  

如果满足以下至少一项条件,则while语句可以正常完成:

     

while语句是可到达的,并且条件表达式不是值为true的常量表达式(第15.28节)。

     

有一个可达的break语句退出while语句。

在无聊的情况下,lambda实际上是块lambda。

在第一种情况下,可以看到,有一个while循环,其循环带有一个值为while的常量表达式(没有true语句),因此无法正常完成(减$ 14.21);它也没有return语句,因此第一个lambda是 value-compatible

同时,根本没有break语句,因此它也是 void兼容的。因此,最后,在第一种情况下,lambda既是无效的又是值兼容的

在第二种情况下,从编译器的角度来看,return循环可以正常完成(因为循环表达式不再是常量表达式),因此,它的全部可以正常完成,因此它不是值兼容的块。但是它仍然是与无效对象兼容的块,因为它不包含任何while语句。

中间结果是,在第一种情况下,lambda既是 void兼容的块又是 value-compatible块;在第二种情况下,它无效兼容块

回顾我们前面提到的内容,这意味着在第一种情况下,lambda将与returnCallable 可能兼容;在第二种情况下,lambda将仅与Runnable 潜在兼容

选择最具体的方法

对于第一种情况,编译器必须在这两种方法之间进行选择,因为这两种方法均可能可能适用。它使用称为“选择最具体的方法”的过程来进行操作,该过程在$ 15.12.2.5中进行了描述。这是摘录:

  

如果T不是S的子类型并且以下条件之一为真(其中U1 ... Uk和R1是参数类型,并且S的捕获函数类型的返回类型,而V1 ... Vk和R2是T的函数类型的参数类型和返回类型):

     

如果e是显式类型的lambda表达式(第15.27.1节),则下列条件之一为真:

     

R2是无效的。

首先

  

显式键入带有零参数的lambda表达式。

此外,RunnableRunnable都不是彼此的子类,并且Callable的返回类型为Runnable,因此我们有一个匹配项: { {1}}比void 更具体。这意味着在第一种情况下,将在CallableRunnable之间选择带有submit(Callable)的方法。

对于第二种情况,我们只有一个可能适用的方法submit(Runnable),因此选择了它。

那为什么会出现变化?

因此,最后,我们可以看到在这些情况下,编译器选择了不同的方法。在第一种情况下,lambda推断为在其Callable方法上具有submit(Runnable)的{​​{1}},因此Callable的调用得以编译。在第二种情况下,throws Exception没有call()声明任何可抛出的异常,因此编译器抱怨没有捕获到异常。