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


210

在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
    }
}

现在,可以争论说这两个接口的行为方式相同,只是在on 和on上Interface2建立了一个契约,这比契约要强一点。当然,我们也可能会争辩说,实现不应对具体的实现状态做出任何假设,或者这样的关键字根本无法发挥作用。method1()method2()Interface1default

题:

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


1
同步是一种实现行为,它会更改编译器生成的最终字节代码结果,因此可以在代码旁边使用。方法声明没有意义。如果同步在抽象层上,编译器产生的结果将令人困惑。
马丁·斯特雷伊克

@MartinStrejc:这可能是省略的一种解释default synchronized,但不一定是这样static synchronized,尽管我会接受出于一致性的原因而省略了后者。
卢卡斯·埃德

1
我不确定这个问题是否会添加任何值,因为synchronized修饰符可能会在子类中被覆盖,因此,只有在最终默认方法中存在某些问题时,才有意义。(您的另一个问题)
2014年

@skiwi:最重要的参数是不够的。子类可以覆盖synchronized在超类中声明的方法,从而有效地消除同步。我不惊讶不支持synchronized与不支持final相关,但是,可能是因为多重继承(例如,继承void x() synchronized void x()等)。但这只是猜测。如果有一个合理的理由,我很好奇。
卢卡斯·埃德

2
>>“子类可以重写在超类中声明为已同步的方法,从而有效地消除了同步”……仅当它们不调用时super,这需要完全重新实现并可能访问私有成员。顺便说一句,将这些方法称为“防御者”是有原因的-存在它们是为了使添加新方法更加容易。
2014年

Answers:


260

虽然乍一看似乎很想synchronized在默认方法上支持修饰符,但事实证明这样做很危险,因此被禁止。

同步方法是一种方法的简写,该方法的行为就像将整个主体包围在一个synchronized块中,该块的锁定对象是接收者。将这种语义扩展到默认方法似乎也很明智。毕竟,它们也是带有接收器的实例方法。(请注意,synchronized方法完全是语法优化;不需要它们,它们比相应的synchronized块更紧凑。有一个合理的论点是,这首先是一个过早的语法优化,并且同步方法造成的问题比他们解决的问题还多,但那艘船早就航行了。)

那么,为什么会有危险呢?同步与锁定有关。锁定是关于协调对可变状态的共享访问。每个对象应具有一个同步策略,该策略确定哪个锁保护哪些状态变量。(请参阅《Java并发实践》第2.4节。)

许多对象使用Java监控器模式(JCiP 4.1)作为其同步策略,其中对象的状态由其内部锁定来保护。这种模式没有什么魔力或特殊之处,但它很方便,并且synchronized在方法上使用关键字会隐式采用这种模式。

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

很难一直为单个源文件维护同步策略。确保子类正确遵守其父类定义的同步策略甚至更加困难。在这样的松散耦合的类(一个接口以及可能实现它的许多类)之间尝试这样做几乎是不可能的,并且容易出错。

考虑到所有这些反对意见,那将是什么理由呢?似乎它们主要是关于使接口的行为更像特征。虽然这是可以理解的愿望,但是默认方法的设计中心是接口演变,而不是“特质”。我们力求做到这两个方面能够始终如一地实现,但是当一个与另一个发生冲突时,我们必须选择支持主要的设计目标。


26
还要注意,在JDK 1.1中,synchronized方法修饰符出现在javadoc输出中,使人们误以为它是规范的一部分。这在JDK 1.2中已修复。即使synchronized修饰符出现在公共方法上,它也是实现的一部分,而不是合同的一部分。(对native修饰符进行类似的推理和处理。)
Stuart Marks

14
早期Java程序中的一个常见错误是,在程序中撒满了足够多的synchronized线程并使用了安全组件,因此您有了几乎是线程安全的程序。问题是这通常可以正常工作,但以令人惊讶和脆弱的方式破裂了。我同意理解锁定的工作方式是鲁棒应用程序的关键。
彼得·劳瑞

10
@BrianGoetz很好的理由。但是为什么synchronized(this) {...}允许使用default方法呢?(如Lukas的问题所示。)这样是否也不允许默认方法拥有实现类的状态?我们也不想阻止这种情况吗?我们是否需要一个FindBugs规则来查找不了解情况的开发人员要做的情况?
2014年

17
@Geoffrey:不,没有理由对此进行限制(尽管应始终谨慎使用。)sync块要求作者显式选择一个锁定对象。如果他们知道该策略是什么,则这将允许他们参与其他对象的同步策略。危险的部分是假设同步“ this”(同步方法所做的事情)实际上是有意义的。这需要做出更明确的决定。也就是说,我希望接口方法中的同步块非常少见。
Brian Goetz 2014年

6
@GeoffreyDeSmet:由于相同的原因,您可以执行例如synchronized(vector)。为了安全起见,切勿使用公用对象(例如其this自身)进行锁定。
2014年

0
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对象具有不同的对象锁。每个锁都是独立的。所以在这种情况下,我认为在父类或通用接口中使用同步并不危险。有人能解释更多吗?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.