Java 8 接口方法中不允许“同步”的原因是啥?

Posted

技术标签:

【中文标题】Java 8 接口方法中不允许“同步”的原因是啥?【英文标题】:What is the reason why “synchronized” is not allowed in Java 8 interface methods?Java 8 接口方法中不允许“同步”的原因是什么? 【发布时间】:2014-06-20 15:20:05 【问题描述】:

在 Java 8 中,我可以轻松编写:

interface Interface1 
    default void method1() 
        synchronized (this) 
            // Something
        
    

    static void method2() 
        synchronized (Interface1.class) 
            // Something
        
    

我将获得我也可以在类中使用的完整同步语义。但是,我不能在方法声明中使用 synchronized 修饰符:

interface Interface2 
    default synchronized void method1() 
        //  ^^^^^^^^^^^^ Modifier 'synchronized' not allowed here
    

    static synchronized void method2() 
        // ^^^^^^^^^^^^ Modifier 'synchronized' not allowed here
    

现在,人们可以争辩说这两个接口的行为方式相同,只是Interface2method1()method2() 上建立了一个合同,这比@987654327 强一点@ 做。当然,我们也可能认为default 实现不应该对具体的实现状态做出任何假设,或者这样的关键字根本不会发挥作用。

问题:

JSR-335 专家组决定在接口方法上不支持synchronized 的原因是什么?

【问题讨论】:

同步是一种实现行为,它改变了编译器生成的最终字节码结果,因此可以在代码旁边使用。它在方法声明中没有意义。如果同步在抽象层上,编译器会产生什么应该会令人困惑。 @MartinStrejc:这可能是省略default synchronized的解释,但不一定是static synchronized,尽管我接受后者可能已被省略一致性原因。 我不确定这个问题是否增加了任何价值,因为 synchronized 修饰符可能会在子类中被覆盖,因此只有作为最终默认方法才有意义。 (你的另一个问题) @skiwi:最重要的参数是不够的。子类可以覆盖在超类中声明为synchronized 的方法,从而有效地消除同步。不过,不支持synchronized 和不支持final 是相关的,我不会感到惊讶,这可能是因为多重继承(例如继承void x() synchronized void x() 等)。但那是猜测。我很好奇一个权威的原因,如果有的话。 >>“子类可能会覆盖在超类中声明为同步的方法,从而有效地消除同步”...仅当它们不调用 super 时,这需要完全重新实现和可能的访问权限给私人会员。顺便说一句,这些方法被称为“防御者”是有原因的——它们的存在是为了更容易添加新方法。 【参考方案1】:

虽然一开始似乎很明显人们希望在默认方法上支持synchronized 修饰符,但事实证明这样做很危险,因此被禁止。

同步方法是一种方法的简写,它的行为就像整个主体都包含在一个synchronized 块中,其锁定对象是接收者。将这种语义扩展到默认方法似乎也是明智的;毕竟,它们也是带有接收器的实例方法。 (请注意,synchronized 方法完全是一种语法优化;它们不是必需的,它们只是比相应的 synchronized 块更紧凑。有一个合理的论点可以证明这是一个过早的语法优化地点,而且同步方法导致的问题比解决的问题多,但那艘船很久以前就航行了。)

那么,为什么它们很危险?同步是关于锁定的。锁定是关于协调对可变状态的共享访问。每个对象都应该有一个同步策略来确定哪些锁保护哪些状态变量。 (参见Java Concurrency in Practice,第 2.4 节。)

许多对象使用 Java Monitor Pattern (JCiP 4.1) 作为它们的同步策略,其中对象的状态由其内在锁保护。这种模式没有什么神奇或特别之处,但它很方便,并且在方法上使用synchronized 关键字隐含地假定了这种模式。

拥有状态的类可以确定该对象的同步策略。但是接口不拥有它们所混入的对象的状态。因此,在接口中使用同步方法假定了一个特定的同步策略,但是你没有合理的假设基础,所以很可能是这样的情况同步的使用不会提供任何额外的线程安全性(您可能在错误的锁上同步)。这会给你一种错误的自信感,认为你已经做了一些关于线程安全的事情,并且没有错误消息告诉你你假设了错误的同步策略。

一致地维护单个源文件的同步策略已经足够困难了;更难确保子类正确遵守其超类定义的同步策略。试图在这种松散耦合的类(一个接口和可能实现它的许多类)之间这样做几乎是不可能的并且非常容易出错。

考虑到所有这些反对的论点,你的论点是什么?似乎它们主要是关于使接口的行为更像特征。虽然这是一个可以理解的愿望,但默认方法的设计中心是接口演变,而不是“Traits--”。在两者可以始终如一地实现的情况下,我们努力做到这一点,但在一个与另一个发生冲突的情况下,我们必须选择有利于主要设计目标。

【讨论】:

还要注意,在 JDK 1.1 中,synchronized 方法修饰符出现在 javadoc 输出中,误导人们认为它是规范的一部分。这已在 JDK 1.2 中修复。即使它出现在公共方法中,synchronized 修饰符也是实现的一部分,而不是合同的一部分。 (native 修饰符也发生了类似的推理和处理。) 早期 Java 程序中的一个常见错误是使用足够多的synchronized 和线程安全组件,而您的程序几乎是线程安全的。问题是这通常可以正常工作,但它以令人惊讶和脆弱的方式破裂。我同意了解锁定的工作原理是强大应用程序的关键。 @BrianGoetz 非常好的理由。但是为什么synchronized(this) ... 允许在default 方法中使用? (如 Lukas 的问题所示。)难道不允许默认方法也拥有实现类的状态吗?难道我们也不想阻止吗?我们是否需要一个 FindBugs 规则来查找不知情的开发人员这样做的情况? @Geoffrey:不,没有理由限制这一点(尽管应始终小心使用。)同步块要求作者明确选择锁定对象;这允许他们参与某个其他对象的同步策略,如果他们知道该策略是什么的话。危险的部分是假设在“this”上进行同步(这是同步方法所做的)实际上是有意义的;这需要一个更明确的决定。也就是说,我希望接口方法中的同步块非常少见。 @GeoffreyDeSmet:出于同样的原因,您可以这样做,例如synchronized(vector)。如果您希望安全,则永远不要使用公共对象(例如this 本身)进行锁定。【参考方案2】:
public class ParentSync 

public synchronized void parentStart() 
    System.out.println("I am " + this.getClass() + " . parentStarting. now:" + nowStr());
    try 
        Thread.sleep(30000);
     catch (InterruptedException e) 
        e.printStackTrace();
    
    System.out.println("I am " + this.getClass() + " . parentFinished. now" + nowStr());


private String nowStr() 
    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());




public class SonSync1 extends ParentSync 
public void sonStart() 
    System.out.println("I am " + this.getClass() + ". sonStarting,calling parent now ... ");
    super.parentStart();
    System.out.println("I am " + this.getClass() + ". sonFinished");





public class SonSync2 extends ParentSync 

public void sonStart() 
    System.out.println("I am " + this.getClass() + ". sonStarting,calling parent now ... ");
    super.parentStart();
    System.out.println("I am " + this.getClass() + ". sonFinished");





public class SyncTest 
public static void main(String[] args) throws Exception 

    new Thread(() -> 
        new SonSync1().sonStart();
    ).start();

    new Thread(() -> 
        new SonSync2().sonStart();
    ).start();

    System.in.read();


结果:

I am class com.common.interface18_design.whynotsync_onmethod.SonSync1. sonStarting,calling parent now ... 
I am class com.common.interface18_design.whynotsync_onmethod.SonSync2. sonStarting,calling parent now ... 
I am class com.common.interface18_design.whynotsync_onmethod.SonSync2 . parentStarting. now:2019-04-18 09:50:08
I am class com.common.interface18_design.whynotsync_onmethod.SonSync1 . parentStarting. now:2019-04-18 09:50:08
I am class com.common.interface18_design.whynotsync_onmethod.SonSync1 . parentFinished. now2019-04-18 09:50:38
I am class com.common.interface18_design.whynotsync_onmethod.SonSync1. sonFinished
I am class com.common.interface18_design.whynotsync_onmethod.SonSync2 . parentFinished. now2019-04-18 09:50:38
I am class com.common.interface18_design.whynotsync_onmethod.SonSync2. sonFinished

(抱歉以父类为例)

从结果可以知道,父类锁是每个子类都拥有的,SonSync1 和 SonSync2 对象有不同的对象锁。每个锁都是独立的。所以在这种情况下,我认为在父类或公共接口中使用同步并不危险。有人可以对此进行更多解释吗?

【讨论】:

以上是关于Java 8 接口方法中不允许“同步”的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章

为什么Java 8接口方法中不允许“final”?

Java 8 中使用的功能接口是啥?

在“应用程序想使用您当前的位置”中不允许点击的委托方法是啥?

IE浏览器在弹出窗口时提示:不支持此接口 window.open()方法是啥原因,以前用得挺好的

为啥接口方法不能是“静态的”和“最终的”?

为啥在java中不允许分配给'this'?