Що є причиною того, що в інтерфейсах 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
Синхронізована поведінка реалізації, і вона змінює результат остаточного байтового коду, зроблений компілятором, щоб його можна було використовувати поруч з кодом. Це не має сенсу в оголошенні методу. Це має заплутати те, що виробляє компілятор, якщо синхронізовано на шарі абстракції.
Мартін Стрейц

@MartinStrejc: Це може бути поясненням пропущення default synchronized, але необов'язково для цього static synchronized, хоча я б погоджувався, що остання може бути опущена з міркувань послідовності.
Лукас Едер

1
Я не впевнений, чи додає це запитання якесь значення, оскільки synchronizedмодифікатор може бути переопрацьований у підкласах, отже, це матиме значення лише за наявності кінцевих методів за замовчуванням. (Ваше інше питання)
skiwi

@skiwi: Переважний аргумент недостатній. Підкласи можуть змінювати методи, які оголошені synchronizedв суперкласах, ефективно знімаючи синхронізацію. Я не був би здивований , що не підтримує synchronizedі не підтримує finalпов'язаний, хоча, можливо , з - за множинного успадкування (наприклад , успадкування void x() і synchronized void x() т.д.). Але це міркування. Мені цікаво авторитетна причина, якщо вона є.
Лукас Едер

2
>> "Підкласи можуть змінювати методи, які оголошені синхронізованими в суперкласах, ефективно видаляючи синхронізацію" ... тільки якщо вони не викликають, superщо вимагає повної повторної реалізації та можливого доступу до приватних членів. До речі, є причина, що ці методи називаються "захисниками" - вони існують, щоб додати нові методи.
bestsss

Відповіді:


260

Хоча спочатку може здатися очевидним, що хочеться підтримати synchronizedмодифікатор методами за замовчуванням, але виявляється, що це було б небезпечно, і так заборонено.

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

Отже, чому вони небезпечні? Синхронізація стосується блокування. Блокування стосується узгодження спільного доступу до змінного стану. Кожен об'єкт повинен мати політику синхронізації, яка визначає, який замок захищає, які змінні стану. (Див. Розділ 2.4. Конкурс Java на практиці , розділ 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: Ні, немає причин обмежувати це (хоча це завжди слід використовувати обережно.) Блок синхронізації вимагає від автора явного вибору об'єкта блокування; це дозволяє їм брати участь у політиці синхронізації якогось іншого об'єкта, якщо вони знають, що це за політика. Небезпечна частина припускає, що синхронізація "цього" (що і роблять методи синхронізації) насправді має сенс; це має бути більш чітким рішенням. Однак, я очікую, що блоки синхронізації в інтерфейсних методах є досить рідкісними.
Брайан Гец

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 мають різний блокування об'єктів. кожен замок - незалежність. тому в цьому випадку я думаю, що це не небезпечно, використовуючи синхронізований у батьківському класі або загальний інтерфейс. хто-небудь може пояснити більше з цього приводу?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.