为什么Java 8接口方法不允许使用"synchronized"关键字?

224
在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()建立了一个合同,比Interface1更加强大以外,这两个接口的行为方式是相同的。当然,我们也可以认为default实现不应该对具体实现状态做出任何假设,或者这样一个关键字根本无法发挥作用。

问题:

JSR-335专家组为什么决定不支持接口方法上的synchronized


2
Synchronized是一种实现行为,它改变了编译器生成的最终字节码结果,因此可以在代码旁边使用。在方法声明中没有意义。如果在抽象层上使用synchronized,可能会让人感到困惑编译器生成了什么。 - Martin Strejc
@MartinStrejc:这可能是省略default synchronized的解释,但不一定适用于static synchronized,尽管我认为后者可能是出于一致性考虑而被省略。 - Lukas Eder
2
我不确定这个问题是否有任何价值,因为synchronized修饰符可能会在子类中被覆盖,因此只有在存在final默认方法时才会有影响。(你的另一个问题) - skiwi
@skiwi:覆盖参数不足。子类可能会覆盖在超类中声明为synchronized的方法,从而有效地删除同步。我不会感到惊讶,如果不支持synchronized和不支持final是相关的,也许是因为多重继承(例如继承void x()synchronized void x()等)。但这只是猜测。如果有权威的原因,我很好奇。 - Lukas Eder
2
子类可以覆盖在超类中声明为同步的方法,有效地去除同步...但前提是不调用super,这需要完全重新实现并可能需要访问私有成员。顺便说一下,这些方法之所以被称为“defenders”(守护者),是为了方便添加新方法。 - bestsss
2个回答

280

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

synchronized方法是一个简写形式,它表现得就像整个方法体被包含在一个锁定的块中,其锁对象是接收器。将这种语义扩展到默认方法似乎是有道理的;毕竟,它们也是具有接收器的实例方法。(注意,synchronized方法完全是一种语法优化;它们不是必需的,只是比相应的synchronized块更紧凑。可以合理地认为,这是一种过早的语法优化,synchronized方法引起的问题比解决的问题还要多,但这艘船已经在很久以前启航了。)

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

许多对象使用Java Monitor Pattern (JCiP 4.1) 作为它们的同步策略,其中对象的状态由其内在锁保护。这种模式并没有什么神奇或特殊之处,但它很方便,在方法上使用 synchronized 关键字隐含地假定了这种模式。
拥有状态的类决定了对象的同步策略。但接口不拥有它们混入的对象的状态。因此,在接口中使用同步方法假定了特定的同步策略,但你没有合理的基础来假定它,所以同步可能根本不提供任何额外的线程安全性(你可能正在对错误的锁进行同步)。这会给你一种错误的信心,认为你已经处理了线程安全问题,而且没有错误消息告诉你正在假定错误的同步策略。
为单个源文件保持同步策略已经足够困难了;确保子类正确遵守其超类定义的同步策略更加困难。试图在这些松散耦合的类之间(一个接口和实现它的可能多个类)这样做几乎是不可能的,并且容易出错。

考虑到所有这些反对的论点,有哪些支持的论点呢?似乎它们大多是关于使接口更像特质的行为。虽然这是可以理解的愿望,但默认方法的设计中心是接口演化,而不是“特质-”。在两者都能够一致实现的地方,我们努力去做到,但在一个与另一个相冲突的地方,我们必须选择支持主要的设计目标。


27
请注意,在JDK 1.1中,文档输出中出现了synchronized方法修饰符,使人们误以为它是规范的一部分。这个问题在JDK 1.2中得到了修复。即使它出现在公共方法上,synchronized修饰符也是实现的一部分,而不是合同的一部分。(类似的推理和处理也发生在native修饰符上。) - Stuart Marks
16
在早期的Java程序中,常见的错误是在各处添加足够的synchronized和线程安全组件,以便使程序几乎具备线程安全性。问题在于,这种方法通常可以正常运行,但会以出乎意料且容易出错的方式破坏程序。我认为,了解锁定过程是构建健壮应用程序的关键。 - Peter Lawrey
11
@BrianGoetz 很好的理由。但是为什么在default方法中允许synchronized(this) {...}呢?(就像Lukas的问题所示。)这不也允许了默认方法拥有实现类的状态吗?我们不也想要防止这种情况发生吗?我们需要一个 FindBugs 规则来找到未经通知的开发人员做出这样的事情吗? - Geoffrey De Smet
@StuartMarks:这确实是一个非常有趣的细节!我不知道Javadoc输出中发生了这种变化。 - Lukas Eder
19
@Geoffrey: 没有理由限制这个(尽管它应该始终小心使用)。同步块需要作者显式选择锁对象;这使他们能够参与某些其他对象的同步策略,如果他们知道那个策略是什么。 危险的部分是假设在 'this' 上同步(这就是同步方法所做的)实际上是有意义的;这需要更明确的决定。 话虽如此,我预计接口方法中的同步块会非常少见。 - Brian Goetz
6
出于相同的原因,您可以使用synchronized(vector)。如果想要安全,就不应该使用公共对象(比如this本身)进行锁定。 - Yogu

-1
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对象具有不同的对象锁。每个锁都是独立的。因此,在这种情况下,我认为在父类或公共接口中使用synchronized并不危险。是否有人能够解释得更详细一些呢?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接