Какова стоимость синхронизации вызова синхронного метода из синхронизированного метода?
Есть ли разница в производительности между этим
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() без каких-либо конфликтов (потому что это только поток, который может достичь этой точки за один конкретный момент времени). Таким образом, синхронизация по ней не имеет эффекта.