Jaki jest powód, dla którego „synchronizacja” jest niedozwolona w metodach interfejsu Java 8?

210

W Javie 8 mogę łatwo napisać:

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

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

Otrzymam pełną semantykę synchronizacji, której mogę używać także na zajęciach. Nie mogę jednak użyć synchronizedmodyfikatora do deklaracji metod:

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

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

Teraz, można argumentować, że te dwa interfejsy zachowują się tak samo, z wyjątkiem, że Interface2ustanawia umowę w sprawie method1()oraz w sprawie method2(), która jest nieco silniejsze niż to, co Interface1robi. Oczywiście możemy również argumentować, że defaultimplementacje nie powinny przyjmować żadnych założeń dotyczących konkretnego stanu implementacji lub że takie słowo kluczowe po prostu nie zwiększy jego wagi.

Pytanie:

Jaki jest powód, dla którego grupa ekspertów JSR-335 postanowiła nie obsługiwać synchronizedmetod interfejsu?

Lukas Eder
źródło
1
Zsynchronizowane to zachowanie implementacyjne, które zmienia wynik końcowy kodu bajtowego wygenerowany przez kompilator, dzięki czemu można go używać obok kodu. Nie ma to sensu w deklaracji metody. Powinno być mylące, co produkuje kompilator, jeśli synchronizacja odbywa się na warstwie abstrakcji.
Martin Strejc
@MartinStrejc: To może być wytłumaczenie dla pominięcia default synchronized, ale niekoniecznie dla static synchronized, chociaż zgodziłbym się, że ten drugi mógł zostać pominięty ze względów spójności.
Lukas Eder
1
Nie jestem pewien, czy to pytanie dodaje jakąkolwiek wartość, ponieważ synchronizedmodyfikator może zostać zastąpiony w podklasach, dlatego miałoby to znaczenie tylko wtedy, gdyby istniało coś jako ostateczne domyślne metody. (Twoje drugie pytanie)
skiwi
@skiwi: Nadrzędny argument nie jest wystarczający. Podklasy mogą zastępować metody zadeklarowane synchronizedw superklasach, skutecznie usuwając synchronizację. Nie zdziwiłbym się jednak, że brak wsparcia synchronizedi brak wsparcia finaljest powiązany, być może z powodu wielokrotnego dziedziczenia (np. Dziedziczenie void x() i synchronized void x() itp.). Ale to spekulacje. Jestem ciekawy wiarygodnego powodu, jeśli taki istnieje.
Lukas Eder
2
>> „Podklasy mogą zastępować metody zadeklarowane jako zsynchronizowane w superklasach, skutecznie usuwając synchronizację” ... tylko wtedy, gdy nie wywołują, superco wymaga pełnej ponownej implementacji i możliwego dostępu do członków prywatnych. Poza tym istnieje powód, dla którego metody te nazywane są „obrońcami” - są one obecne, aby umożliwić łatwiejsze dodawanie nowych metod.
bestsss

Odpowiedzi:

260

Choć na początku może wydawać się oczywiste, że ktoś chciałby obsługiwać synchronizedmodyfikator w metodach domyślnych, okazuje się, że takie postępowanie byłoby niebezpieczne i dlatego było zabronione.

Metody zsynchronizowane są skrótem dla metody, która zachowuje się tak, jakby całe ciało było zamknięte w synchronizedbloku, którego obiektem blokady jest odbiornik. Rozsądne może być rozszerzenie tej semantyki na metody domyślne; w końcu są to również metody instancji z odbiornikiem. (Zauważ, że synchronizedmetody są całkowicie optymalizacją składniową; nie są potrzebne, są po prostu bardziej zwarte niż odpowiedni synchronizedblok. Należy uzasadnić argument, że była to przede wszystkim przedwczesna optymalizacja składniowa i że metody synchronizowane powodują więcej problemów niż rozwiązują, ale ten statek płynął dawno temu.)

Dlaczego są niebezpieczne? Synchronizacja polega na blokowaniu. Blokowanie polega na koordynowaniu wspólnego dostępu do stanu zmiennego. Każdy obiekt powinien mieć strategię synchronizacji, która określa, które blokady chronią zmienne stanu. (Zobacz Współbieżność Java w praktyce , sekcja 2.4.)

Wiele obiektów stosuje jako swoją strategię synchronizacji wzorzec monitorowania Java (JCiP 4.1), w którym stan obiektu jest chroniony przez jego wewnętrzną blokadę. W tym wzorze nie ma nic magicznego ani specjalnego, ale jest to wygodne, a użycie synchronizedsłowa kluczowego w metodach domyślnie zakłada ten wzór.

Jest to klasa, która jest właścicielem stanu, który może określić zasady synchronizacji tego obiektu. Ale interfejsy nie są właścicielami stanu obiektów, w które się wmieszają. Zatem użycie metody synchronicznej w interfejsie zakłada określoną zasadę synchronizacji, ale taką, na którą nie masz uzasadnionych podstaw, więc może się zdarzyć, że użycie synchronizacji nie zapewnia żadnego dodatkowego bezpieczeństwa wątku (być może synchronizacja odbywa się na niewłaściwej blokadzie). To dałoby fałszywe poczucie pewności, że zrobiłeś coś z bezpieczeństwem wątków, a żaden komunikat o błędzie nie mówi, że zakładasz niewłaściwą politykę synchronizacji.

Jest już wystarczająco trudne, aby konsekwentnie utrzymywać zasady synchronizacji dla jednego pliku źródłowego; jeszcze trudniej jest upewnić się, że podklasa poprawnie przestrzega zasad synchronizacji określonych przez jej nadklasę. Próba zrobienia tego między takimi luźno powiązanymi klasami (interfejs i prawdopodobnie wiele klas, które go implementują) byłaby prawie niemożliwa i bardzo podatna na błędy.

Biorąc pod uwagę wszystkie te argumenty przeciwko, jaki byłby argument? Wygląda na to, że chodzi głównie o to, aby interfejsy zachowywały się bardziej jak cechy. Chociaż jest to zrozumiałe pragnienie, centrum projektowania metod domyślnych jest ewolucja interfejsu, a nie „Cechy…”. Tam, gdzie te dwa cele można było konsekwentnie osiągnąć, staraliśmy się to zrobić, ale tam, gdzie jeden jest w konflikcie z drugim, musieliśmy wybrać główny cel projektowania.

Brian Goetz
źródło
26
Zauważ też, że w JDK 1.1 synchronizedmodyfikator metody pojawił się w wyjściu javadoc, wprowadzając ludzi w błąd, myśląc, że jest to część specyfikacji. Zostało to naprawione w JDK 1.2. Nawet jeśli pojawia się w metodzie publicznej, synchronizedmodyfikator jest częścią implementacji, a nie kontraktu. (Podobne uzasadnienie i leczenie miały miejsce w przypadku nativemodyfikatora).
Stuart Marks
15
Częstym błędem we wczesnych programach Java było rozsypywanie wystarczającej liczby synchronizedelementów bezpiecznych dla wątków, a program miał prawie wątkowy. Problem polegał na tym, że zwykle działało to dobrze, ale pękało w zaskakujący i kruchy sposób. Zgadzam się, że zrozumienie, jak działa blokowanie, jest kluczem do niezawodnych aplikacji.
Peter Lawrey,
10
@BrianGoetz Bardzo dobry powód. Ale dlaczego metoda jest synchronized(this) {...}dozwolona default? (Jak pokazano w pytaniu Lukasa). Czy to nie pozwala, aby domyślna metoda była również właścicielem stanu klasy implementacji? Czy my też nie chcemy temu zapobiec? Czy potrzebujemy reguły FindBugs, aby znaleźć przypadki, w których robią to niedoinformowani programiści?
Geoffrey De Smet
17
@Geoffrey: Nie, nie ma powodu, aby to ograniczać (choć zawsze należy tego używać ostrożnie). Blok synchronizacji wymaga od autora jawnego wyboru obiektu blokady; pozwala im to uczestniczyć w polityce synchronizacji innego obiektu, jeśli wiedzą, co to jest polityka. Niebezpieczną częścią jest założenie, że synchronizacja na „tym” (co robią metody synchronizacji) jest rzeczywiście znacząca; musi to być bardziej jednoznaczna decyzja. To powiedziawszy, spodziewam się, że bloki synchronizacji w metodach interfejsu będą dość rzadkie.
Brian Goetz
6
@GeoffreyDeSmet: Z tego samego powodu możesz zrobić np synchronized(vector). Jeśli chcesz być bezpieczny, nigdy nie powinieneś używać obiektu publicznego (takiego jak on thissam) do blokowania.
Yogu
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();
}
}

wynik:

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

(przepraszam za użycie klasy nadrzędnej jako przykładu)

z tego wynika, że ​​blokada klasy nadrzędnej jest własnością każdej podklasy, SonSync1 i obiekt SonSync2 mają inną blokadę obiektu. każdy zamek jest niezależny. więc w tym przypadku myślę, że używanie synchronizacji w klasie nadrzędnej lub wspólnego interfejsu nie jest niebezpieczne. czy ktoś mógłby wyjaśnić więcej na ten temat?

zhenke zhu
źródło