Какова стоимость синхронизации вызова синхронного метода из синхронизированного метода?

Есть ли разница в производительности между этим

synchronized void x() {
    y();
}

synchronized void y() {
}

и этот

synchronized void x() {
    y();
}

void y() {
}

Ответы

Ответ 1

Да, есть дополнительная стоимость исполнения, если только JVM не включит вызов y(), который современный JIT-компилятор сделает в довольно коротком порядке. Сначала рассмотрим случай, который вы представили, в котором y() отображается вне класса. В этом случае JVM должен проверить ввод y(), чтобы он мог войти в монитор на объекте; эта проверка всегда будет успешной, если вызов поступает из x(), но ее нельзя пропустить, поскольку вызов может поступать от клиента вне класса. Эта дополнительная проверка берет на себя небольшую стоимость.

Кроме того, рассмотрим случай, когда y() равен private. В этом случае компилятор по-прежнему не оптимизирует синхронизацию; см. следующую разборку пустой y():

private synchronized void y();
  flags: ACC_PRIVATE, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
       0: return

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

Запись монитора при вызове метода synchronized и выход монитора по его возврату обрабатываются неявно с помощью команд вызова и возврата Java Virtual Machine, как если бы использовался монитор и monitorexit.

@AmolSonawane правильно отмечает, что JVM может оптимизировать этот код во время выполнения, выполнив укрупнение блокировки, существенно введя метод y(). В этом случае, после того, как JVM решила выполнить оптимизацию JIT, вызовы от x() до y() не будут нести дополнительные накладные расходы на производительность, но, конечно, вызовы непосредственно в y() из любого другого места все равно должны будут приобрести монитор отдельно.

Ответ 2

Результаты микро-тест с jmh

Benchmark                      Mean     Mean error    Units
c.a.p.SO18996783.syncOnce      21.003        0.091  nsec/op
c.a.p.SO18996783.syncTwice     20.937        0.108  nsec/op

= > нет статистической разницы.

Глядя на сгенерированную сборку, показано, что блокировка укрупнения была выполнена и y_sync была встроена в x_sync, хотя она синхронизирована.

Полные результаты:

Benchmarks: 
# Running: com.assylias.performance.SO18996783.syncOnce
Iteration   1 (5000ms in 1 thread): 21.049 nsec/op
Iteration   2 (5000ms in 1 thread): 21.052 nsec/op
Iteration   3 (5000ms in 1 thread): 20.959 nsec/op
Iteration   4 (5000ms in 1 thread): 20.977 nsec/op
Iteration   5 (5000ms in 1 thread): 20.977 nsec/op

Run result "syncOnce": 21.003 ±(95%) 0.055 ±(99%) 0.091 nsec/op
Run statistics "syncOnce": min = 20.959, avg = 21.003, max = 21.052, stdev = 0.044
Run confidence intervals "syncOnce": 95% [20.948, 21.058], 99% [20.912, 21.094]

Benchmarks: 
com.assylias.performance.SO18996783.syncTwice
Iteration   1 (5000ms in 1 thread): 21.006 nsec/op
Iteration   2 (5000ms in 1 thread): 20.954 nsec/op
Iteration   3 (5000ms in 1 thread): 20.953 nsec/op
Iteration   4 (5000ms in 1 thread): 20.869 nsec/op
Iteration   5 (5000ms in 1 thread): 20.903 nsec/op

Run result "syncTwice": 20.937 ±(95%) 0.065 ±(99%) 0.108 nsec/op
Run statistics "syncTwice": min = 20.869, avg = 20.937, max = 21.006, stdev = 0.052
Run confidence intervals "syncTwice": 95% [20.872, 21.002], 99% [20.829, 21.045]

Ответ 3

Почему бы не проверить это!? Я быстро провел тест. Метод benchmark() вызывается в цикле для разминки. Это может быть не очень точно, но оно показывает некоторый непротиворечивый интерес.

public class Test {
    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            System.out.println("+++++++++");
            benchMark();
        }
    }

    static void benchMark() {
        Test t = new Test();
        long start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            t.x();
        }
        System.out.println("Double sync:" + (System.nanoTime() - start) / 1e6);

        start = System.nanoTime();
        for (int i = 0; i < 100; i++) {
            t.x1();
        }
        System.out.println("Single sync:" + (System.nanoTime() - start) / 1e6);
    }
    synchronized void x() {
        y();
    }
    synchronized void y() {
    }
    synchronized void x1() {
        y1();
    }
    void y1() {
    }
}

Результаты (последние 10)

+++++++++
Double sync:0.021686
Single sync:0.017861
+++++++++
Double sync:0.021447
Single sync:0.017929
+++++++++
Double sync:0.021608
Single sync:0.016563
+++++++++
Double sync:0.022007
Single sync:0.017681
+++++++++
Double sync:0.021454
Single sync:0.017684
+++++++++
Double sync:0.020821
Single sync:0.017776
+++++++++
Double sync:0.021107
Single sync:0.017662
+++++++++
Double sync:0.020832
Single sync:0.017982
+++++++++
Double sync:0.021001
Single sync:0.017615
+++++++++
Double sync:0.042347
Single sync:0.023859

Похоже, вторая вариация действительно немного быстрее.

Ответ 4

Тест можно найти ниже (вы должны угадать, что делают некоторые методы, но ничего сложного):

Он проверяет их по 100 потоков каждый и начинает подсчет средних значений после завершения 70% из них (как разминка).

Он печатает его один раз в конце.

public static final class Test {
        final int                      iterations     =     100;
        final int                      jiterations    = 1000000;
        final int                      count          = (int) (0.7 * iterations);
        final AtomicInteger            finishedSingle = new AtomicInteger(iterations);
        final AtomicInteger            finishedZynced = new AtomicInteger(iterations);
        final MovingAverage.Cumulative singleCum      = new MovingAverage.Cumulative();
        final MovingAverage.Cumulative zyncedCum      = new MovingAverage.Cumulative();
        final MovingAverage            singleConv     = new MovingAverage.Converging(0.5);
        final MovingAverage            zyncedConv     = new MovingAverage.Converging(0.5);

        // -----------------------------------------------------------
        // -----------------------------------------------------------
        public static void main(String[] args) {
                final Test test = new Test();

                for (int i = 0; i < test.iterations; i++) {
                        test.benchmark(i);
                }

                Threads.sleep(1000000);
        }
        // -----------------------------------------------------------
        // -----------------------------------------------------------

        void benchmark(int i) {

                Threads.async(()->{
                        long start = System.nanoTime();

                        for (int j = 0; j < jiterations; j++) {
                                a();
                        }

                        long elapsed = System.nanoTime() - start;
                        int v = this.finishedSingle.decrementAndGet();
                        if ( v <= count ) {
                                singleCum.add (elapsed);
                                singleConv.add(elapsed);
                        }

                        if ( v == 0 ) {
                                System.out.println(elapsed);
                                System.out.println("Single Cum:\t\t" + singleCum.val());
                                System.out.println("Single Conv:\t" + singleConv.val());
                                System.out.println();

                        }
                });

                Threads.async(()->{

                        long start = System.nanoTime();
                        for (int j = 0; j < jiterations; j++) {
                                az();
                        }

                        long elapsed = System.nanoTime() - start;

                        int v = this.finishedZynced.decrementAndGet();
                        if ( v <= count ) {
                                zyncedCum.add(elapsed);
                                zyncedConv.add(elapsed);
                        }

                        if ( v == 0 ) {
                                // Just to avoid the output not overlapping with the one above 
                                Threads.sleep(500);
                                System.out.println();
                                System.out.println("Zynced Cum: \t"  + zyncedCum.val());
                                System.out.println("Zynced Conv:\t" + zyncedConv.val());
                                System.out.println();
                        }
                });

        }                       

        synchronized void a() { b();  }
                     void b() { c();  }
                     void c() { d();  }
                     void d() { e();  }
                     void e() { f();  }
                     void f() { g();  }
                     void g() { h();  }
                     void h() { i();  }
                     void i() { }

        synchronized void az() { bz(); }
        synchronized void bz() { cz(); }
        synchronized void cz() { dz(); }
        synchronized void dz() { ez(); }
        synchronized void ez() { fz(); }
        synchronized void fz() { gz(); }
        synchronized void gz() { hz(); }
        synchronized void hz() { iz(); }
        synchronized void iz() {}
}

MovingAverage.Cumulative add в основном (выполняется атомарно): average = (среднее * (n) + число)/(++ n);

MovingAverage.Converging вы можете искать, но использует другую формулу.

Результаты после 50-секундного разминки:

С: jiterations → 1000000

Zynced Cum:     3.2017985649516254E11
Zynced Conv:    8.11945143126507E10

Single Cum:     4.747368153507841E11
Single Conv:    8.277793176290959E10

Среднее значение nano секунд. Это действительно ничего и даже показывает, что zynced занимает меньше времени.

С: jiterations → original * 10 (требуется гораздо больше времени)

Zynced Cum:     7.462005651190714E11
Zynced Conv:    9.03751742946726E11

Single Cum:     9.088230941676143E11
Single Conv:    9.09877020004914E11

Как вы можете видеть, результаты показывают, что это действительно не большая разница. Для последнего 30% -ного заселения на самом деле имеет нижнее среднее время.

С одним потоком каждый (итерации = 1) и jiterations = original * 100;

Zynced Cum:     6.9167088486E10
Zynced Conv:    6.9167088486E10

Single Cum:     6.9814404337E10
Single Conv:    6.9814404337E10

В той же среде потока (удаление вызовов Threads.async)

С: jiterations → original * 10

Single Cum:     2.940499529542545E8
Single Conv:    5.0342450600964054E7


Zynced Cum:     1.1930525617915475E9
Zynced Conv:    6.672312498662484E8

Замкнутый здесь кажется медленнее. По порядку ~ 10. Причина этого может быть вызвана тем, что каждый из них запускается каждый раз, кто знает. Нет энергии, чтобы попробовать обратное.

Последний прогон теста:

public static final class Test {
        final int                      iterations     =     100;
        final int                      jiterations    = 10000000;
        final int                      count          = (int) (0.7 * iterations);
        final AtomicInteger            finishedSingle = new AtomicInteger(iterations);
        final AtomicInteger            finishedZynced = new AtomicInteger(iterations);
        final MovingAverage.Cumulative singleCum      = new MovingAverage.Cumulative();
        final MovingAverage.Cumulative zyncedCum      = new MovingAverage.Cumulative();
        final MovingAverage            singleConv     = new MovingAverage.Converging(0.5);
        final MovingAverage            zyncedConv     = new MovingAverage.Converging(0.5);

        // -----------------------------------------------------------
        // -----------------------------------------------------------
        public static void main(String[] args) {
                final Test test = new Test();

                for (int i = 0; i < test.iterations; i++) {
                        test.benchmark(i);
                }

                Threads.sleep(1000000);
        }
        // -----------------------------------------------------------
        // -----------------------------------------------------------

        void benchmark(int i) {

                        long start = System.nanoTime();

                        for (int j = 0; j < jiterations; j++) {
                                a();
                        }

                        long elapsed = System.nanoTime() - start;
                        int s = this.finishedSingle.decrementAndGet();
                        if ( s <= count ) {
                                singleCum.add (elapsed);
                                singleConv.add(elapsed);
                        }

                        if ( s == 0 ) {
                                System.out.println(elapsed);
                                System.out.println("Single Cum:\t\t" + singleCum.val());
                                System.out.println("Single Conv:\t" + singleConv.val());
                                System.out.println();

                        }


                        long zstart = System.nanoTime();
                        for (int j = 0; j < jiterations; j++) {
                                az();
                        }

                        long elapzed = System.nanoTime() - zstart;

                        int z = this.finishedZynced.decrementAndGet();
                        if ( z <= count ) {
                                zyncedCum.add(elapzed);
                                zyncedConv.add(elapzed);
                        }

                        if ( z == 0 ) {
                                // Just to avoid the output not overlapping with the one above 
                                Threads.sleep(500);
                                System.out.println();
                                System.out.println("Zynced Cum: \t"  + zyncedCum.val());
                                System.out.println("Zynced Conv:\t" + zyncedConv.val());
                                System.out.println();
                        }

        }                       

        synchronized void a() { b();  }
                     void b() { c();  }
                     void c() { d();  }
                     void d() { e();  }
                     void e() { f();  }
                     void f() { g();  }
                     void g() { h();  }
                     void h() { i();  }
                     void i() { }

        synchronized void az() { bz(); }
        synchronized void bz() { cz(); }
        synchronized void cz() { dz(); }
        synchronized void dz() { ez(); }
        synchronized void ez() { fz(); }
        synchronized void fz() { gz(); }
        synchronized void gz() { hz(); }
        synchronized void hz() { iz(); }
        synchronized void iz() {}
}

Заключение, действительно нет разницы.

Ответ 5

В случае, когда оба метода синхронизированы, вы будете блокировать монитор дважды. Поэтому первый подход снова потребует дополнительных накладных расходов. Но ваша JVM может снизить стоимость блокировки путем укрупнения блокировки и может вызывать прямой вызов y().

Ответ 6

Никакой разницы не будет. Поскольку потоки содержат только для фиксации в x(). Поток, который приобрел блокировку в x(), может получить блокировку в y() без каких-либо конфликтов (потому что это только поток, который может достичь этой точки за один конкретный момент времени). Таким образом, синхронизация по ней не имеет эффекта.