Статьи

Финальные поля Java на x86 нет оп?

Мне всегда нравилось копаться в деталях многопоточного программирования, и мне всегда нравилось, что, несмотря на долгие годы чтения о моделях согласованности памяти ЦП, алгоритмах без ожидания и без блокировки, модели памяти java, параллельности java на практике и т. Д. и т.д. — я все еще создаю ошибки многопоточного программирования. Это всегда удивительно унизительный опыт, который напоминает мне, насколько сложна эта проблема.

Если вы читали JMM, то, возможно, помните, что одной из областей, которую они усилили, была гарантия видимости конечных полей после завершения конструктора. Например,

1
2
3
4
5
6
7
8
9
public class ClassA {
   public final String b;
 
   public ClassA(String b) {
      this.b = b;
   }
}
...
ClassA x = new ClassA("hello");

JMM заявляет, что каждый поток (даже потоки, отличные от того, который создал экземпляр x класса ClassA)
всегда отмечайте xb как «привет» и никогда не увидите значение null (значение по умолчанию для ссылочного поля).

Это действительно здорово! Это означает, что мы можем создавать неизменяемые объекты, просто помечая поля как окончательные, и любой созданный экземпляр автоматически может быть разделен между потоками без какой-либо другой работы, чтобы гарантировать видимость памяти. Woot! Обратной стороной этого является то, что если бы ClassA.b не был помечен как окончательный, то у вас не было бы такой гарантии. И другие потоки могли наблюдать нулевой результат xb == (если никакие другие механизмы «безопасной публикации» не использовались для получения видимости)

Когда они создали новый JMM, любимый участник JCP, Дуг Ли, создал кулинарную книгу, чтобы помочь разработчикам JVM реализовать новые правила модели памяти. Если вы прочитаете это, то увидите, что «правила» гласят, что JIT-компиляторы должны испускать барьер памяти StoreStore , прямо перед возвращением конструктора. Этот барьер StoreStore является своего рода «забором памяти». Когда указывается в инструкциях по сборке, это означает, что никакие операции записи (сохранения) в память после перегородки не могут быть переупорядочены до того, как операции записи в память появятся перед ограничителем. Обратите внимание, что это ничего не говорит о чтениях — они могут «прыгать» забор в любом направлении.

Так что это значит? хорошо, если вы думаете о том, что делает компилятор, когда вы вызываете конструктор:

1
2
3
4
5
6
7
8
9
String x = new ClassA("hello");
  get's broken down in to pseudo-code steps of:
 
1. pointer_to_A = allocate memory for ClassA
    (mark word, class object pointer, one reference field for String b)
2. pointer_to_A.whatever class meta data = ...
3. pointer_to_A.b = address of "hello" string
4. emit a StoreStore memory barrier per the JMM
5. x = pointer_to_A

Барьер StoreStore на шаге 4 гарантирует, что любые записи (такие как метаданные класса и поле b) не будут переупорядочены с записью в x на шаге 5. Это то, что гарантирует, что если x виден любым другим потокам — что этот другой поток не может видеть x и без xb. Без барьера памяти StoreStore шаги 3 и 5 можно переупорядочить, и запись в основную память для x может появиться перед записью в xb и другим процессором. ядро может наблюдать, что pointer_to_A.b равен 0 (нулю), что нарушает JMM.

Отличные новости! Однако, если вы посмотрите на эту кулинарную книгу, вы увидите некоторые интересные вещи: (1) многие люди пишут JVM на множестве процессорных архитектур! (2) * Все * барьеры памяти на x86 не являются операционными, кроме барьера StoreLoad! Это означает, что на x86 вышеупомянутый барьер памяти StoreStore не используется, и поэтому для него не создается сборка. Это ничего не делает! Это связано с тем, что модель памяти x86 представляет собой строгий «общий порядок магазинов» (TSO). X86 следит за тем, чтобы все записи в память наблюдались так, как если бы они были выполнены в одном и том же порядке. Таким образом, запись 5 никогда не будет появляться перед 3 в любом другом потоке в любом случае из-за TSO, и нет необходимости создавать ограничение памяти. Другие архитектуры ЦП имеют более слабые модели памяти, которые не дают таких гарантий, и, таким образом, необходим ограничитель памяти StoreStore. Обратите внимание, что более слабые модели памяти, хотя, возможно, сложнее или менее интуитивно понятны для программирования, как правило, гораздо быстрее, поскольку процессор может переупорядочивать объекты, чтобы более эффективно использовать записи в кэш и снижать согласованность работы кеша.

Очевидно, вы должны продолжать писать правильный код, следующий за JMM. НО это также означает (к сожалению или к счастью), что забывание этого не приведет к ошибкам, если вы работаете на x86 … как я делаю на работе.

Чтобы по-настоящему раскрыть этот дом и убедиться, что нет других побочных эффектов, которые, возможно, не описаны в кулинарной книге, я запустил средство вывода сборки x86, как описано здесь, и записал результат вызова конструктора для ClassA (с последним на поле ссылочного типа) и конструктор для ClassB, который был идентичен ClassA, за исключением последнего ключевого слова в классе. Выход сборки x86 идентичен . Таким образом, с точки зрения JIT, на x86 (не itanium, не arm и т. Д.) Последнее ключевое слово не оказывает влияния.

Если вам интересно, как выглядит ассемблерный код здесь, он ниже. Обратите внимание на отсутствие каких-либо заблокированных инструкций. Когда Oracle 7u25 JRE испускает ограничение памяти StoreLoad x86, это делается с помощью генерации блокировки addl $ 0x0, (% rsp), которая просто добавляет ноль к указателю стека — без операции, но, поскольку она заблокирована — это имеет эффект полного забор (который соответствует критериям забора StoreLoad). В x86 есть несколько разных способов вызвать эффект полного забора, и они обсуждаются в списке рассылки OpenJDK. Они заметили, что, по крайней мере, в Nehelem Intel добавление блокировки 0 было наиболее компактным / эффективным.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
0x00007f152c020c60: mov    %eax,-0x14000(%rsp)
  0x00007f152c020c67: push   %rbp
  0x00007f152c020c68: sub    $0x20,%rsp         ;*synchronization entry
                                                ; - com.argodata.match.profiling.FinalConstructorMain::callA@-1 (line 60)
  0x00007f152c020c6c: mov    %rdx,(%rsp)
  0x00007f152c020c70: mov    %esi,%ebp
  0x00007f152c020c72: mov    0x60(%r15),%rax
  0x00007f152c020c76: mov    %rax,%r10
  0x00007f152c020c79: add    $0x18,%r10
  0x00007f152c020c7d: cmp    0x70(%r15),%r10
  0x00007f152c020c81: jae    0x00007f152c020cd6
  0x00007f152c020c83: mov    %r10,0x60(%r15)
  0x00007f152c020c87: prefetchnta 0xc0(%r10)
  0x00007f152c020c8f: mov    $0x8356f3d0,%r11d  ;   {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}
  0x00007f152c020c95: mov    0xb0(%r11),%r10
  0x00007f152c020c9c: mov    %r10,(%rax)
  0x00007f152c020c9f: movl   $0x8356f3d0,0x8(%rax)  ;   {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}
  0x00007f152c020ca6: mov    %r12d,0x14(%rax)   ;*new  ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)
  0x00007f152c020caa: mov    %ebp,0xc(%rax)     ;*putfield a
                                                ; - com.argodata.match.profiling.FinalConstructorMain$ClassA::@6 (line 17)
                                                ; - com.argodata.match.profiling.FinalConstructorMain::callA@6 (line 60)
  0x00007f152c020cad: mov    (%rsp),%r10
  0x00007f152c020cb1: mov    %r10d,0x10(%rax)   ;*new  ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)
  0x00007f152c020cb5: mov    %rax,%r10
  0x00007f152c020cb8: shr    $0x9,%r10
  0x00007f152c020cbc: mov    $0x7f152b765000,%r11
  0x00007f152c020cc6: mov    %r12b,(%r11,%r10,1)  ;*synchronization entry
                                                ; - com.argodata.match.profiling.FinalConstructorMain::callA@-1 (line 60)
  0x00007f152c020cca: add    $0x20,%rsp
  0x00007f152c020cce: pop    %rbp
  0x00007f152c020ccf: test   %eax,0x9fb932b(%rip)        # 0x00007f1535fda000
                                                ;   {poll_return}
  0x00007f152c020cd5: retq  
  0x00007f152c020cd6: mov    $0x8356f3d0,%rsi   ;   {oop('com/argodata/match/profiling/FinalConstructorMain$ClassA')}
  0x00007f152c020ce0: xchg   %ax,%ax
  0x00007f152c020ce3: callq  0x00007f152bfc51e0  ; OopMap{[0]=Oop off=136}
                                                ;*new  ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)
                                                ;   {runtime_call}
  0x00007f152c020ce8: jmp    0x00007f152c020caa  ;*new
                                                ; - com.argodata.match.profiling.FinalConstructorMain::callA@0 (line 60)
  0x00007f152c020cea: mov    %rax,%rsi
  0x00007f152c020ced: add    $0x20,%rsp
  0x00007f152c020cf1: pop    %rbp
  0x00007f152c020cf2: jmpq   0x00007f152bfc8920  ;   {runtime_call}