Javenue logo

Javenue

Программирование на Java

Информационные технологии

Синхронизированный метод и блок в Java (synchronized method vs block)

Уже не раз слышал от java-программистов, что синхронизированный метод это просто более удобный способ записи synchronized блока с синхронизацией по this.

То есть некоторые люди считают, что объявление

class A {
  synchronized void method() { }
}

равноценно следующему:

class A {
  void method() {
    synchronized (this) { }
  }
}

Давайте разберемся с этим раз и навсегда. А поможет нам в этом замечательная книжка, которую каждый человек не зависимо от рода деятельности наверняка носит с собой в бумажном или электронном виде, - "The Java Virtual Machine Specification" :).

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

То есть для этих классов как минимум будут сгенерированы разные последовательности opcode. Проверим наше предположение.

Дизассемблирование скомпилированного class-файла можно произвести с помощью команды: javap -c <имя класса>

Вот результат дизассемблирования для первого класса:

class A extends java.lang.Object{
A();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."":()V
   4:   return

synchronized void method();
  Code:
   0:   return

}

Для второго класса результат будет таким:

class A extends java.lang.Object{
A();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."":()V
   4:   return

void method();
  Code:
   0:   aload_0
   1:   dup
   2:   astore_1
   3:   monitorenter
   4:   aload_1
   5:   monitorexit
   6:   goto    14
   9:   astore_2
   10:  aload_1
   11:  monitorexit
   12:  aload_2
   13:  athrow
   14:  return
  Exception table:
   from   to  target type
     4     6     9   any
     9    12     9   any

}

Инструкции monitorenter и monitorexit используются для захвата и освобождения монитора соотвтественно. Второй monitorexit необходим для особождения монитора в случае возникновения исключения (exception / error) в synchronized блоке.

То есть утверждение о равноценности записей уже не является верным.

Возникает другой вопрос, каким же образом происходит захват монитора в первом случае. И еще - какой из вариантов будет работать быстрее.

Тут уже простыми догадками не обойтись - придется обратиться к спецификации виртаульной машины Java (The Java Virtual Machine Specification).

В разделе "7.14 Synchronization" читаем следующее:

1. A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the runtime constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the current thread acquires a monitor, invokes the method itself, and releases the monitor whether the method invocation completes normally or abruptly...

Кроме того, из раздела "8.13 Locks and Synchronization":

2. "A synchronized method automatically performs a lock operation when it is invoked; its body is not executed until the lock operation has successfully completed..."

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

Но, к сожалению, получить какие-то показательные результаты в короткие сроки у меня не получилось, а времени свободного маловато. Буду дальше читать, разбираться и пробовать. Ждите продолжение...

P.S. Как обычно, комментарии и замечания приветствуются.



Комментариев: 4

  Выйти

  * для публикации комментариев нужно  

dob:

Вот прочитал, и не совсем понял… В чем же первый вариант должен быть быстрее? Допустим у вас несколько десятков потоков, которые вызывают один и тот же метод. А сам по себе метод содержит лишь один критический участок (доступ к общему ресурсу). Вся остальная часть - вспомогательные вычисления. Ну это так, для образности =) (а ведь именно это встречается сплошь и рядом). И в первом случае (использование synchronized метода) получается, что все потоки, кроме одного будут ждать освобождения монитора вместо того, что бы выполнять независимые друг от друга операции?
Имхо все от реализации зависит.

c0nst:

В обоих случаях будет происходить захват монитора и только один поток одновременно сможет выполнять критическую секцию.

Но, последовательность opcode в случае нормальной работы в первом случае будет содержать одну инструкцию, во втором - восемь:

0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
14: return

Определение объекта, у которого необходимо захватить монитор, в первом случае не имеет смысла, так как объект нам уже известен (мы собственно вызываем его метод). А синхронизированный блок в качестве монитора может иметь все что угодно (в данном случае - this).

То есть как минимум пара инструкций экономятся.

Updated: А, я понял, что вы имеете в виду. Согласен. Но в статье говорится о заблуждении, что синхронизированный метод равноценен синхронизированному блоку, который полностью содержит тело метода, с синхронизацией по this.

Sergey:

c0nst, авторы книги A programmer’s guide to Java SCJP Certification… с вами не согласны (глава 13.5, параграф Synchronized blocks), да и я тоже. Всё-таки

synchronized void method()
{
...
}

и

void method() {
synchronized (this) {...}
}

это логически одно и то же, то есть такая замена никогда не изменит результатов выполнения.

Также я не разделяю ваших рассуждений о производительности, ведь в обоих случаях буду выполнятся одни и те же действия (захват монитора, освобождение монитора), просто во втором случае эти действия явно прописаны в байт-коде, а в первом JVM сама знает, что их нужно выполнить.

Если я не прав, поправьте меня.

codemo:

Да, на хабре уже упоминалось в комментах к 5 незнаниям о многопоточности Java.
Более того, этот вопрос поднимался на весенней встрече в питерском отделении разработчиков Java, инженер был очень удивлён этим мифом, семантически они эквивалентны, байт код - да - разный, но - как уже упоминалось - на хабре было подвтерждение что нативный код генерится одинаковый.