По какой причине синхронизация не допускается в интерфейсных методах 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
    }
}

Теперь можно утверждать , что два интерфейса ведут себя таким же образом , за исключением того, что Interface2устанавливает контракт на method1()и на method2(), что немного сильнее , чем Interface1делает. Конечно, мы могли бы также утверждать, что defaultреализации не должны делать никаких предположений о конкретном состоянии реализации, или что такое ключевое слово просто не будет тянуть свой вес.

Вопрос:

По какой причине экспертная группа JSR-335 решила не поддерживать synchronizedметоды интерфейса?

Лукас Эдер
источник
1
Synchronized - это поведение реализации, которое изменяет конечный результат байт-кода, созданный компилятором, поэтому его можно использовать рядом с кодом. Это не имеет смысла в объявлении метода. Это должно сбивать с толку то, что производит компилятор, если синхронизируется на уровне абстракции.
Мартин Стрейц,
@MartinStrejc: Это могло бы быть объяснением для опущения default synchronized, но не обязательно для него static synchronized, хотя я бы согласился, что последнее могло быть опущено по соображениям согласованности.
Лукас Эдер
1
Я не уверен, добавляет ли этот вопрос какое-либо значение, так как synchronizedмодификатор может быть переопределен в подклассах, следовательно, это будет иметь значение только в том случае, если есть что-то в качестве окончательных методов по умолчанию. (Ваш другой вопрос)
скиви
@skiwi: главного аргумента недостаточно. Подклассы могут переопределять методы, объявленные synchronizedв суперклассах, эффективно удаляя синхронизацию. Я не удивлюсь, что не поддержка synchronizedи не поддержка finalсвязаны, хотя, может быть, из-за множественного наследования (например, наследование void x() и synchronized void x() т. Д.). Но это предположение. Мне любопытно по поводу авторитетной причины, если она есть.
Лукас Эдер
2
>> «Подклассы могут переопределять методы, которые объявлены синхронизированными в суперклассах, эффективно удаляя синхронизацию» ... только если они не вызывают, superчто требует полной повторной реализации и возможного доступа к закрытым членам. Кстати, есть причина, по которой эти методы называются «защитниками» - они присутствуют, чтобы облегчить добавление новых методов.
bestsss

Ответы:

260

Хотя на первый взгляд может показаться очевидным, что кто-то захочет поддерживать synchronizedмодификатор в методах по умолчанию, оказывается, что это будет опасно и поэтому запрещено.

Синхронизированные методы - это сокращение для метода, который ведет себя так, как будто все тело заключено в synchronizedблок, чей объект блокировки является получателем. Может показаться целесообразным расширить эту семантику и на методы по умолчанию; в конце концов, они тоже являются экземплярами методов с получателем. (Обратите внимание, что synchronizedметоды являются полностью синтаксической оптимизацией; они не нужны, они просто более компактны, чем соответствующий synchronizedблок. Есть разумный аргумент, чтобы, во-первых, это была преждевременная синтаксическая оптимизация, и что синхронизированные методы вызывают больше проблем, чем решают, но этот корабль давно отплыл.)

Итак, почему они опасны? Синхронизация о блокировке. Блокировка - это координация общего доступа к изменяемому состоянию. Каждый объект должен иметь политику синхронизации, которая определяет, какие блокировки защищают, какие переменные состояния. (См. Java Concurrency на практике , раздел 2.4.)

Многие объекты используют в качестве своей политики синхронизации шаблон монитора Java (JCiP 4.1), в котором состояние объекта защищается его внутренней блокировкой. В этом шаблоне нет ничего волшебного или особенного, но он удобен, и использование synchronizedключевого слова для методов неявно предполагает этот шаблон.

Это класс, которому принадлежит состояние, которое определяет политику синхронизации этого объекта. Но интерфейсам не принадлежит состояние объектов, в которые они смешаны. Поэтому использование синхронизированного метода в интерфейсе предполагает определенную политику синхронизации, но такую, которую у вас нет разумных оснований для принятия, поэтому вполне может быть так, что использование синхронизации не обеспечивает никакой дополнительной безопасности потока (вы можете синхронизироваться с неправильной блокировкой). Это даст вам ложное чувство уверенности в том, что вы что-то сделали с безопасностью потоков, и ни одно сообщение об ошибке не сообщит вам, что вы принимаете неправильную политику синхронизации.

Уже достаточно сложно постоянно поддерживать политику синхронизации для одного исходного файла; еще труднее обеспечить, чтобы подкласс правильно придерживался политики синхронизации, определенной его суперклассом. Попытка сделать это между такими слабо связанными классами (интерфейсом и, возможно, многими классами, которые его реализуют) была бы почти невозможной и подверженной ошибкам.

Учитывая все эти аргументы против, для чего будет аргумент? Кажется, они в основном делают интерфейсы более похожими на черты. Хотя это понятное желание, центром разработки методов по умолчанию является развитие интерфейса, а не «черты». В тех случаях, когда эти два фактора могли быть последовательно достигнуты, мы стремились к этому, но там, где одно находится в конфликте с другим, нам приходилось выбирать в пользу основной цели проектирования.

Брайан Гетц
источник
26
Также обратите внимание, что в JDK 1.1 synchronizedмодификатор метода появился в выводе javadoc, вводя людей в заблуждение, что он является частью спецификации. Это было исправлено в JDK 1.2. Даже если он появляется в открытом методе, synchronizedмодификатор является частью реализации, а не контракта. (Подобное рассуждение и обработка произошли для nativeмодификатора.)
Стюарт Маркс
15
Распространенная ошибка в ранних программах на Java заключалась в том, чтобы разбрызгивать достаточно synchronizedи создавать потоки безопасных компонентов, и у вас была почти поточно-безопасная программа. Проблема была в том, что это обычно работало нормально, но ломалось удивительными и хрупкими способами. Я согласен, что понимание того, как работает ваша блокировка, является ключом к надежным приложениям.
Питер Лори
10
@BrianGoetz Очень веская причина. Но почему это synchronized(this) {...}разрешено в defaultметоде? (Как показано в вопросе Лукаса.) Разве это не позволяет методу по умолчанию владеть состоянием класса реализации? Разве мы не хотим предотвратить это тоже? Нам понадобится правило FindBugs, чтобы найти случаи, для которых неосведомленные разработчики делают это?
Джеффри Де Смет
17
@ Geoffrey: Нет, нет никаких причин ограничивать это (хотя это всегда следует использовать с осторожностью.) Блок синхронизации требует от автора явного выбора объекта блокировки; это позволяет им участвовать в политике синхронизации какого-либо другого объекта, если они знают, что это за политика. Опасная часть предполагает, что синхронизация на «this» (что и делают методы синхронизации) действительно имеет смысл; это должно быть более четкое решение. Тем не менее, я ожидаю, что блоки синхронизации в интерфейсных методах будут довольно редкими.
Брайан Гетц
6
@ GeoffreyDeSmet: по той же причине, что вы можете сделать, например synchronized(vector). Если вы хотите быть в безопасности, вы никогда не должны использовать публичный объект (такой как он thisсам) для блокировки.
Йогу
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 имеют различную блокировку объекта. каждый замок независим. так что в этом случае, я думаю, не опасно использовать синхронизированный в родительском классе или общий интерфейс. Кто-нибудь может объяснить больше об этом?

женке жу
источник