Аннотация:
Книга будет полезна в первую очередь для Java разработчиков. Все повествование в книге идет вокруг джавовских примитивов синхронизации и классов, полезных при написании параллельных программ.
Re: Brian Goetz и др. Java Concurrency in Practice
Здравствуйте, Михаил Воронцов, Вы писали:
МВ>Книга будет полезна в первую очередь для Java разработчиков. Все повествование в книге идет вокруг джавовских примитивов синхронизации и классов, полезных при написании параллельных программ.
1. Скажите пожалуйста, после прочтения аннотации у меня сложилось впечатление, что книга ставит своей целью просто рассказать о классах из java.util.concurrent и его подпакетов. Так ли это?
2. Как вы считаете, на какой уровень расчитана эта книга? Вопрос возник потому, что в приведенном примере баг. Если представить ситуацию, когда вычисления объемны и длительны по времени, то возможно следующее -- клиент вызывает compute(), начинаются расчеты. После этого N других клиентов делают запрос на вычисление по тому же аргументу. Если расчеты длятся недопустимо долго, то данные уже могут стать не нужны первому клиенту. Он отменяет свой запрос. При этом мы с определенной вероятностью попадаем в бесконечный цикл.
Пример:
import java.util.concurrent.*;
/**
* @author Denis Zhdanov
*/public class CCC {
private final ConcurrentHashMap<String, Future<Void>> cache = new ConcurrentHashMap<String, Future<Void>>();
public static void main(String[] args) throws Exception {
final CCC ccc = new CCC();
Thread impatientThread = new Thread() {
public void run() {
System.out.println("Impatient client requested for data");
try {
ccc.compute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println("Starting impatient client. ThreadId=" + System.identityHashCode(impatientThread));
impatientThread.start();
for (final int i : new int[] {1, 2}) {
new Thread() {
public void run() {
System.out.println("Starting client " + i + ". ThreadId=" + System.identityHashCode(this));
System.out.println("Client " + i + " requested for data");
try {
ccc.compute();
System.out.println("Client " + i + " received response");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
Thread.sleep(1000);
System.out.println("Impatient client cancels request for data");
impatientThread.interrupt();
}
public Void compute() throws InterruptedException {
while (true) {
Future<Void> f = cache.get("1");
if (f == null) {
Callable<Void> eval = new Callable<Void>() {
public Void call() throws InterruptedException {
System.out.println("Computation started");
Thread.sleep(3000);
System.out.println("Computation ended");
return null;
}
};
FutureTask<Void> ft = new FutureTask<Void>(eval);
f = cache.putIfAbsent("1", ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
System.out.println("CancellationException, threadId=" + System.identityHashCode(Thread.currentThread()));
cache.remove("1", f);
} catch (ExecutionException e) {
// cache.remove("1", f); (1)
System.out.println("ExecutionException, threadId=" + System.identityHashCode(Thread.currentThread()));
}
}
}
}
У меня примерно один раз из десяти случается shit. Shit выглядит так:
Starting impatient client. ThreadId=22837328
Impatient client requested for data
Computation started
Starting client 2. ThreadId=8451275
Client 2 requested for data
Starting client 1. ThreadId=3374351
Client 1 requested for data
Impatient client cancels request for data
ExecutionException, threadId=8451275
ExecutionException, threadId=22837328
ExecutionException, threadId=3374351
ExecutionException, threadId=8451275
ExecutionException, threadId=22837328
ExecutionException, threadId=3374351
ExecutionException, threadId=8451275
ExecutionException, threadId=3374351
ExecutionException, threadId=8451275
ExecutionException, threadId=22837328
etc
Лечится раскомментированием (1). После этого получаем
Starting impatient client. ThreadId=22837328
Impatient client requested for data
Starting client 2. ThreadId=8451275
Client 2 requested for data
Computation started
Starting client 1. ThreadId=3374351
Client 1 requested for data
Impatient client cancels request for data
ExecutionException, threadId=8451275
ExecutionException, threadId=3374351
ExecutionException, threadId=22837328
Computation started
Computation ended
Client 1 received response
Client 2 received response
Здравствуйте, bolshik, Вы писали:
B>1. Скажите пожалуйста, после прочтения аннотации у меня сложилось впечатление, что книга ставит своей целью просто рассказать о классах из java.util.concurrent и его подпакетов. Так ли это?
По сути, можно ответить словами авторов из предисловия к книге (я их привел уже):
Java 5.0 is a huge step forward for the development of concurrent applications in Java, providing new higher-level components and additional low-level mechanisms that make it easier for novices and experts alike to build concurrent applications. The authors are primary members of the JCP Expert Group that created these facilities; in addition to describing their behavior and features, we present the underlying design patterns and anticipated usage scenarios that motivated their inclusion in the platform libraries.
Они действительно делают хороший упор на java.util.concurrent и классы коллекций, но, с другой стороны — ведь это и есть основные "кирпичики" для построения многопоточности в яве (высокоуровневой многопоточности).
В последних главах (начиная с 9 примерно) повествование отходит прямо уж от явного использования java.util.concurrent и обсуждаются более продвинутые вопросы и способы их оптимальной и корректной реализации. Чтение реально полезное и интересное. Из простых вещей, там обсуждавшихся: вы знаете, что в многопоточном коде при использовании несколькими потоками одного экземпляра следующего класса (пример не из книги, но суть оттуда) после вызова метода setter одним потоком, а метода test — другим, test может вернуть true? (речь шла о возможности переупорядочивания операций)
class Test
{
private int a;
private boolean b;
public Test() {a = 0; b = false;}
public void setter() { a = 1; b = true;}
public boolean test() {
if ((a == 0) && (b)) return true;
return false;
}
}
B>2. Как вы считаете, на какой уровень расчитана эта книга? Вопрос возник потому, что в приведенном примере баг. Если представить ситуацию, когда вычисления объемны и длительны по времени, то возможно следующее -- клиент вызывает compute(), начинаются расчеты. После этого N других клиентов делают запрос на вычисление по тому же аргументу. Если расчеты длятся недопустимо долго, то данные уже могут стать не нужны первому клиенту. Он отменяет свой запрос. При этом мы с определенной вероятностью попадаем в бесконечный цикл.
Да, знаю Вот абзац из текста книги:
Caching a Future instead of a value creates the possibility of cache pollution: if a computation is cancelled or fails, future attempts to compute the result will also indicate cancellation or failure. To avoid this, Memoizer removes the Future from the cache if it detects that the computation was cancelled; it might also be desirable to remove the Future upon detecting a RuntimeException if the computation might succeed on a future attempt. Memoizer also does not address cache expiration, but this could be accomplished by using a subclass of FutureTask that associates an expiration time with each result and periodically scanning the cache for expired entries. (Similarly, it does not address cache eviction, where old entries are removed to make room for new ones so that the cache does not consume too much memory.)
Жирным я выделил то, на что Вы указали в коде. Однако, если убрать такие требования, то класс является полностью потокобезопасным, что уже не мало.
Данный пример был приведен в конце 5ой главы. Что касается того, как нужно работать с задачами, у которых есть лимит времени выполнения (6 глава) или как нужно прерывать задачи (7 глава) — так это было дальше по книге
На мой взгляд, книжка рассчитана на людей, понимающих что есть многопоточность, и, наверное, пробовавших нарисовать что-то своё многопоточное.
Первая часть книги описывает то, как нужно писать потокобезопасный код ВНУТРИ потока: как прятать данные, как их лочить и т.д. Вторая часть описывает то, как управлять потоками. Третья и четвертая — всё по-немногу, но на более продвинутом уровне.
Да, кстати говоря, в книге упоминаются и классы, добавленные в Java 1.6.
Re[3]: Brian Goetz и др. Java Concurrency in Practice
Здравствуйте, mik1, Вы писали:
M>...Из простых вещей, там обсуждавшихся: вы знаете, что в многопоточном коде при использовании несколькими потоками одного экземпляра следующего класса (пример не из книги, но суть оттуда) после вызова метода setter одним потоком, а метода test — другим, test может вернуть true? (речь шла о возможности переупорядочивания операций)
Видимо речь идет все же о переключении между потоками.
M>...однако, если убрать такие требования, то класс является полностью потокобезопасным, что уже не мало.
Это, конечно, хорошо, но не так уж и много Мне не нравится сама идея с тем, что вычисления производятся в клиентских потоках, а не в worker thread.
M>На мой взгляд, книжка рассчитана на людей, понимающих что есть многопоточность, и, наверное, пробовавших нарисовать что-то своё многопоточное. M>Первая часть книги описывает то, как нужно писать потокобезопасный код ВНУТРИ потока: как прятать данные, как их лочить и т.д. Вторая часть описывает то, как управлять потоками. Третья и четвертая — всё по-немногу, но на более продвинутом уровне.
M>Да, кстати говоря, в книге упоминаются и классы, добавленные в Java 1.6.
Спасибо.
Здравствуйте, bolshik, Вы писали:
B>Здравствуйте, mik1, Вы писали:
M>>...Из простых вещей, там обсуждавшихся: вы знаете, что в многопоточном коде при использовании несколькими потоками одного экземпляра следующего класса (пример не из книги, но суть оттуда) после вызова метода setter одним потоком, а метода test — другим, test может вернуть true? (речь шла о возможности переупорядочивания операций) B>Видимо речь идет все же о переключении между потоками.
Посмотрите внимательно на то, как поля инициализированы в конструкторе, и на то, в каком порядке их устанавливает setter. Речь идет именно о переупорядочивании операций.
M>>...однако, если убрать такие требования, то класс является полностью потокобезопасным, что уже не мало. B>Это, конечно, хорошо, но не так уж и много Мне не нравится сама идея с тем, что вычисления производятся в клиентских потоках, а не в worker thread.
Ну в данном классе — да. Но опять же — см. мое предыдущее сообщение — класс был описан довольно рано в книге и много не использует. Добавим в Ваш код Executor-а и всё будет в ажуре.
class CCC {
private final ConcurrentHashMap<String, Future<Void>> cache = new ConcurrentHashMap<String, Future<Void>>();
private static final ExecutorService exec = Executors.newFixedThreadPool(2);public static void main2() throws Exception {
final CCC ccc = new CCC();
Thread impatientThread = new Thread() {
public void run() {
System.out.println("Impatient client requested for data");
try {
ccc.compute();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println("Starting impatient client. ThreadId=" + System.identityHashCode(impatientThread));
impatientThread.start();
for (final int i : new int[] {1, 2}) {
new Thread() {
public void run() {
System.out.println("Starting client " + i + ". ThreadId=" + System.identityHashCode(this));
System.out.println("Client " + i + " requested for data");
try {
ccc.compute();
System.out.println("Client " + i + " received response");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
Thread.sleep(1000);
// System.out.println("Impatient client cancels request for data");
// impatientThread.interrupt();exec.shutdown();
}
public Void compute() throws InterruptedException {
while (true) {
Future<Void> f = cache.get("1");
if (f == null) {
Callable<Void> eval = new Callable<Void>() {
public Void call() throws InterruptedException {
System.out.println("Computation started");
Thread.sleep(3000);
System.out.println("Computation ended");
return null;
}
};
System.out.println("1");
Future<Void> ft = exec.submit(eval);
System.out.println("2");
f = cache.putIfAbsent("1", ft);
if (f == null) f = ft;
}
try {
Thread.sleep(1000);
System.out.println("Print 1000, threadId=" + System.identityHashCode(Thread.currentThread()));
Thread.sleep(1000);
System.out.println("Print 2000, threadId=" + System.identityHashCode(Thread.currentThread()));
Thread.sleep(1000);
System.out.println("Print 3000, threadId=" + System.identityHashCode(Thread.currentThread()));
Thread.sleep(4000);
System.out.println("Print 4000, threadId=" + System.identityHashCode(Thread.currentThread()));
return f.get();
} catch (CancellationException e) {
System.out.println("CancellationException, threadId=" + System.identityHashCode(Thread.currentThread()));
cache.remove("1", f);
} catch (ExecutionException e) {
cache.remove("1", f);
System.out.println("ExecutionException, threadId=" + System.identityHashCode(Thread.currentThread()));
}
}
}
}
Название метода main я исправил — мне его так проще было из существующего проекта вызвать. Добавил Executora (тут в его конструктор можно и 1 было передать), подправил код создания объекта Future. Добавил отладочные печати через каждую секунду перед вызовом Future.get. Ну и добавил код гашения потока пула потоков executor-а (не понимаю, почему не гасится сам).
Получил такой вывод на экран:
Starting impatient client. ThreadId=5678233
Impatient client requested for data
1
2
Starting client 1. ThreadId=14746332
Client 1 requested for data
Computation started
Starting client 2. ThreadId=8568863
Client 2 requested for data
Print 1000, threadId=5678233
Print 1000, threadId=14746332
Print 1000, threadId=8568863
Print 2000, threadId=5678233
Print 2000, threadId=14746332
Print 2000, threadId=8568863
Print 3000, threadId=5678233
Computation ended
Print 3000, threadId=14746332
Print 3000, threadId=8568863
Print 4000, threadId=5678233
Print 4000, threadId=14746332
Client 1 received response
Print 4000, threadId=8568863
Client 2 received response
Обрати внимание, что "1" и "2" выводятся подряд — значит exec.submit(eval) запускает отдельный поток. Дальше все потоки параллельно продвигаются к финишу (в Print XXXX все 3 Id потоков).
А по сути согласен с тем замечанием, что в примере вычисления шли НЕ параллельно.
Re[5]: Brian Goetz и др. Java Concurrency in Practice
Здравствуйте, mik1, Вы писали:
M>Посмотрите внимательно на то, как поля инициализированы в конструкторе, и на то, в каком порядке их устанавливает setter. Речь идет именно о переупорядочивании операций.
правильно ли я понимаю, что ты имеешь ввиду, что когда в setter() сначала инициализируется int, потом boolean, авторы считают, что этот порядок противоположен? С моей точки зрения там беда в том, что поля объявлены без модификатора volatile.
M>Ну и добавил код гашения потока пула потоков executor-а (не понимаю, почему не гасится сам).
Потому что имеем:
Здравствуйте, bolshik, Вы писали:
B>Здравствуйте, mik1, Вы писали:
M>>Посмотрите внимательно на то, как поля инициализированы в конструкторе, и на то, в каком порядке их устанавливает setter. Речь идет именно о переупорядочивании операций. B>правильно ли я понимаю, что ты имеешь ввиду, что когда в setter() сначала инициализируется int, потом boolean, авторы считают, что этот порядок противоположен? С моей точки зрения там беда в том, что поля объявлены без модификатора volatile.
На этом примере автор хотел показать что — "There is no guarantee that operations in one thread will be performed in the order given by the program." Это называется 'reordering'.
Присваивание boolean переменной могло происходить раньше чем int (например из-за оптимизации компилятором). И другой поток мог увидеть boolean раньше и получить неверное значение int.
По спецификации JVM (а точнее её части Java Memory Model) volatile не помогала в таких случаях до версии JVM 5.0, ещё в версии 1.4 volatile переменные могли реордерится (http://www-128.ibm.com/developerworks/java/library/j-jtp02244.html — Problem 2) хотя для 1.4 вышла заплатка в виде багфикса, которая меняла это поведение и запрещала реордеринг volatile переменных.
Re[7]: Brian Goetz и др. Java Concurrency in Practice
Здравствуйте, Sergey.Zakharov, Вы писали:
SZ>Здравствуйте, bolshik, Вы писали:
B>>Здравствуйте, mik1, Вы писали:
M>>>Посмотрите внимательно на то, как поля инициализированы в конструкторе, и на то, в каком порядке их устанавливает setter. Речь идет именно о переупорядочивании операций. B>>правильно ли я понимаю, что ты имеешь ввиду, что когда в setter() сначала инициализируется int, потом boolean, авторы считают, что этот порядок противоположен? С моей точки зрения там беда в том, что поля объявлены без модификатора volatile.
SZ>На этом примере автор хотел показать что — "There is no guarantee that operations in one thread will be performed in the order given by the program." Это называется 'reordering'. SZ>Присваивание boolean переменной могло происходить раньше чем int (например из-за оптимизации компилятором). И другой поток мог увидеть boolean раньше и получить неверное значение int. SZ>По спецификации JVM (а точнее её части Java Memory Model) volatile не помогала в таких случаях до версии JVM 5.0, ещё в версии 1.4 volatile переменные могли реордерится (http://www-128.ibm.com/developerworks/java/library/j-jtp02244.html — Problem 2) хотя для 1.4 вышла заплатка в виде багфикса, которая меняла это поведение и запрещала реордеринг volatile переменных.
Сорри, поторопился слегка На самом деле предыдущие виртуальные машины могли реордерить volatile и non-volatile (т.е. если int не volatile, а bool — volatile, тогда код не работает). Две volatile переменные запрещалось реордерить и раньше. В новых VM (5.0), грубо говоря, запрещен также реордеринг volatile и non-volatile переменных. Т.е. после до присваивания чего-либо volatile переменной компилятор обязан сделать все присвоения non-volatile переменных
Re[8]: Brian Goetz и др. Java Concurrency in Practice
Здравствуйте, Sergey.Zakharov, Вы писали:
SZ>Сорри, поторопился слегка На самом деле предыдущие виртуальные машины могли реордерить volatile и non-volatile (т.е. если int не volatile, а bool — volatile, тогда код не работает). Две volatile переменные запрещалось реордерить и раньше. В новых VM (5.0), грубо говоря, запрещен также реордеринг volatile и non-volatile переменных. Т.е. после до присваивания чего-либо volatile переменной компилятор обязан сделать все присвоения non-volatile переменных. В новых VM (5.0), грубо говоря, запрещен также реордеринг volatile и non-volatile переменных.
Реордеринг volalile и не-volalile в некоторых случаях разрешен, а в некоторых нет.
Об этом проще прочитать в статье Doug Lea "The JSR-133 Cookbook for Compiler Writers" http://gee.cs.oswego.edu/dl/jmm/cookbook.html в главе 'Volatiles and Monitors'.
Вот выдержки из статьи:
"The main JMM rules for volatiles and monitors can be viewed as a matrix with cells indicating that
you cannot reorder instructions associated with particular sequences of bytecodes. This table is not
itself the JMM specification; it is just a useful way of viewing its main consequences for compilers
and runtime systems.
------------------------------------------------------------------------------------------------
Can Reorder | 2nd operation |
------------------------------------------------------------------------------------------------
1st operation |Normal Load/Store |Vol Load/MonitorEnter|Vol Store/MonitorExit|
------------------------------------------------------------------------------------------------
Normal Load/Store | | | No |
------------------------------------------------------------------------------------------------
Vol Load/MonitorEnter | No | No | No |
------------------------------------------------------------------------------------------------
Vol Store/MonitorExit | | No | No |
------------------------------------------------------------------------------------------------
Where:
* Normal Loads are getfield, getstatic, array load of non-volatile fields
* Normal Stores are putfield, putstatic, array store of non-volatile fields
* Volatile Loads are getfield, getstatic of volatile fields that are accessible by multiple threads
* Volatile Stores are putfield, putstatic of volatile fields that are accessible by multiple threads
* MonitorEnters (including entry to synchronized methods) are for lock objects accessible by multiple threads.
* MonitorExits (including exit from synchronized methods) are for lock objects accessible by multiple threads.
The cells for Normal Loads are the same as for Normal Stores, those for Volatile Loads are the
same as MonitorEnter, and those for Volatile Stores are same as MonitorExit, so they are collapsed
together here (but are expanded out as needed in subsequent tables).
Any number of other operations might be present between the indicated 1st and 2nd operations in
the table. So, for example, the "No" in cell [Normal Store, Volatile Store] says that a
non-volatile store cannot be reordered with ANY subsequent volatile store; at least any that
can make a difference in multithreaded program semantics."
Здравствуйте, Golovach Ivan, Вы писали:
SZ>>Сорри, поторопился слегка На самом деле предыдущие виртуальные машины могли реордерить volatile и non-volatile (т.е. если int не volatile, а bool — volatile, тогда код не работает). Две volatile переменные запрещалось реордерить и раньше. В новых VM (5.0), грубо говоря, запрещен также реордеринг volatile и non-volatile переменных. Т.е. после до присваивания чего-либо volatile переменной компилятор обязан сделать все присвоения non-volatile переменных. В новых VM (5.0), грубо говоря, запрещен также реордеринг volatile и non-volatile переменных.
GI>Реордеринг volalile и не-volalile в некоторых случаях разрешен, а в некоторых нет. GI>Об этом проще прочитать в статье Doug Lea "The JSR-133 Cookbook for Compiler Writers" http://gee.cs.oswego.edu/dl/jmm/cookbook.html в главе 'Volatiles and Monitors'.
Это не тот документ, куда надо смотреть Java программисту, программисту надо смотреть в спецификацию языка. Тут есть 2 уровня, первый — формальная спецификация для программиста, второй — реализация. Связующее звено между ними — это обеспечение *видимости* выполнения спецификации. Это важно понимать, что есть специфицированное поведение, а что есть — деталь реализации.
Например, если в программе записано деление 2 переменных, то с т.з. программиста это только деление и ничего больше. При генерации же машинного кода это деление может быть представлено делением, умножением, сдвигом, вычислением по таблице, вычисление с частными случаями и т.д. Или, например, вычисления без побочных эффектов могут быть полностью устранены компилятором. Т.е. формально компилятор не делает того, что он него просит программист, но это нормально, пока обеспечивается *видимость* выполнения спецификации.
Обратите внимание на выделенное:
This table is not itself the JMM specification; it is just a useful way of viewing its main consequences for compilers and runtime systems.
И дальше:
So, for example, the "No" in cell [Normal Store, Volatile Store] says that a non-volatile store cannot be reordered with ANY subsequent volatile store; at least any that can make a difference in multithreaded program semantics.
Т.е. компилятор может и не переупорядочивать обычные обращения и volatile обращения. Но может и переупорядочивать, потому что это не влияет на видимое поведение, и у программиста нет никаких средств отличить 2 поведения. Последний абзац говорит даже больше — на самом деле компилятор может делать *любые* переупорядочивания и оптимизации, *пока* они не меняют семантики многопоточной программы.
Например для такого кода:
volatile int X;
X = 1;
X = 2;
Компилятор может убрать строчку ”X = 1;”, т.е. программа никаким образом не сможет это отличить.
Или, например, если компилятор делает escape анализ и может определить, что volatile переменная локальна для потока, то он может трактовать её как обычную переменную и делать с ней любые переупорядочивания.
Т.е., грубо говоря, на уровне реализации с volatile переменными может происходить вообще что угодно. А упомянутая таблица есть лишь наиболее разумное и простое отображение JMM на расслабленную модель памяти (SPARC RMO) для общих случаев.
Т.о. ответ на вопрос могут ли обычные обращения к памяти переупорядочиваться с volatile для программиста — нет, не могут. Другой взгляд на это — некоторые переупорядочивания не возможны, а другие абсолютно не интересны. Просто судить о семантике программы на основе последовательно-консистентной (sequentially consistent) модели памяти (т.е. без переупорядочиваний) на порядок проще, чем на основе расслабленной модели (т.е. с переупорядочиваниями). А это как раз и есть основная идея, стоящая за Java volatile, — обеспечение последовательной консистентности, т.е. отсутствие любых переупорядочиваний, т.е. простота (обмен скорости на простоту использования).
Однако для Java тут есть ещё небольшая оговорка, которая называется, non properly synchronized programs (некорректно синхронизированные программы). Они могут увидеть практически любые переупорядочивания и другие интересные вещи. Но их семантика не особо интересна, т.к. фактически они официально обозваны «плохими» программами, в них содержатся гонки, и единственное намерение по поводу их функционирования — это предотвращение компромитации безопасности и падения JVM (для стороннего «подозрительного» кода). Такая же ситуация и для C#. А, например, для С++ нет проблемы выполнения «подозрительного» кода, поэтому С++ просто полностью запрещает некорректно синхронизированные программы, не давая абсолютно никаких гарантий по поводу их функционирования.
Ещё раз — с т.з. программиста Java volatile обеспечивают последовательную консистентность и не допускают никаких переупорядочиваний. Это то, чем надо руководствоваться; всё остальное — детали реализации.
Здравствуйте, remark, Вы писали: R>Ещё раз — с т.з. программиста Java volatile обеспечивают последовательную консистентность и не допускают никаких переупорядочиваний. Это то, чем надо руководствоваться; всё остальное — детали реализации.
и, как я понимаю, это условие МОЖЕТ ВЫПОЛНИТСЯ. Т.е. с точки зрения второго потока будет виден реордеринг записи vol и записи nonvol.
Для себя я понял JMM так, что нельзя нарушать happend-before и casuality. Оно и не нарушается в данном примере.
Вполне допускаю, что я не прав. Но тогда я заблуждаюсь искренне.
Вы могли бы обосновать свое мнения на моем примере основываясь на спецификации языка?
Здравствуйте, Golovach Ivan, Вы писали:
GI>МММ ... Я еще раз пересмотрел 17-ю главу jls.third_edition [http://java.sun.com/docs/books/jls/third_edition/html/memory.html] и не обнаружил правила запрещающего следующее: GI>Пусть один поток вызывает метод doIt() у общего объекта GI>
GI>class Shared {
GI> public volatile int vol = 0;
GI> public int nonvol = 0;
GI> public void doIt() {
GI> vol = 1;
GI> nonvol = 1;
GI> }
GI>}
GI>
GI>а второй поток проверяет условие у этого же общего объекта GI>
GI>и, как я понимаю, это условие МОЖЕТ ВЫПОЛНИТСЯ. Т.е. с точки зрения второго потока будет виден реордеринг записи vol и записи nonvol. GI>Для себя я понял JMM так, что нельзя нарушать happend-before и casuality. Оно и не нарушается в данном примере. GI>Вполне допускаю, что я не прав. Но тогда я заблуждаюсь искренне. GI>Вы могли бы обосновать свое мнения на моем примере основываясь на спецификации языка?
В приведенном примере указанное условие может выполниться не из-за reordering, а из-за обыкновенного race condition. Т.е. последовательность событий может быть такая:
Thread t1 calls doIt();
Thread t2 checks 'shared.vol == 0 (true)';
t1 executes 'vol = 1; nonvol = 1';
t2 checks 'shared.nonvol ==1 (true)';
В указанном примере happen-before вообще не работает, потому что 'nonvol = 1' выполняется после записи в volatile-переменную.
Здравствуйте, denis.zhdanov, Вы писали:
DZ>Здравствуйте, Golovach Ivan, Вы писали:
GI>>МММ ... Я еще раз пересмотрел 17-ю главу jls.third_edition [http://java.sun.com/docs/books/jls/third_edition/html/memory.html] и не обнаружил правила запрещающего следующее: GI>>Пусть один поток вызывает метод doIt() у общего объекта GI>>
GI>>class Shared {
GI>> public volatile int vol = 0;
GI>> public int nonvol = 0;
GI>> public void doIt() {
GI>> vol = 1;
GI>> nonvol = 1;
GI>> }
GI>>}
GI>>
GI>>а второй поток проверяет условие у этого же общего объекта GI>>
GI>>и, как я понимаю, это условие МОЖЕТ ВЫПОЛНИТСЯ. Т.е. с точки зрения второго потока будет виден реордеринг записи vol и записи nonvol. GI>>Для себя я понял JMM так, что нельзя нарушать happend-before и casuality. Оно и не нарушается в данном примере. GI>>Вполне допускаю, что я не прав. Но тогда я заблуждаюсь искренне. GI>>Вы могли бы обосновать свое мнения на моем примере основываясь на спецификации языка?
DZ>В приведенном примере указанное условие может выполниться не из-за reordering, а из-за обыкновенного race condition. Т.е. последовательность событий может быть такая:
DZ> DZ>Thread t1 calls doIt(); DZ>Thread t2 checks 'shared.vol == 0 (true)'; DZ>t1 executes 'vol = 1; nonvol = 1'; DZ>t2 checks 'shared.nonvol ==1 (true)'; DZ>
Согласен. Предлагаю переформулировку. Мне кажется, что переписанное условие
if (shared.nonvol == 1 && shared.vol == 0) {...}
может выполнится. По крайней мере, я не нашел запрета на это, но нашел подтверждение в виде Roach Motel Principle.
И если это так, что с точки зрения второго потока он будет наблюдать явление, трактуемое как реордеринг записи в vol и nonvol. Т.е. то, что категорически отвергал remark.
DZ>В указанном примере happen-before вообще не работает, потому что 'nonvol = 1' выполняется после записи в volatile-переменную.
Ну да, хаппенд-бефора тут нет. Вопрос же был — возможен ли хоть какой-то реордеринг волатайл и не-волатайл операций.
Собственно, я опираюсь на Roach Motel Principle (тут, например — http://osdir.com/ml/java.jsr.166-concurrency/2008-04/msg00018.html — Автор Джереми Мэйсон):
""Roach Motel" applies to synchronized blocks -- instructions can move
into them, but not out. So, normal accesses can be moved from after a
monitorexit to before it, and from before a monitorenter to after it.
That's the roach motel principle.
The same reasoning applies to volatiles under the assumption that a
volatile write is "the same" as a monitorexit and that a volatile read
is "the same" as a monitorenter.
So, your answer is that you *can* move normal accesses that occur after
a volatile write to before it."
Здравствуйте, Golovach Ivan, Вы писали:
GI>и, как я понимаю, это условие МОЖЕТ ВЫПОЛНИТСЯ. Т.е. с точки зрения второго потока будет виден реордеринг записи vol и записи nonvol. GI>Для себя я понял JMM так, что нельзя нарушать happend-before и casuality. Оно и не нарушается в данном примере. GI>Вполне допускаю, что я не прав. Но тогда я заблуждаюсь искренне. GI>Вы могли бы обосновать свое мнения на моем примере основываясь на спецификации языка?
Во-первых, как уже ответили такой исход (сработает условие во втором потоке) возможен и в последовательно-консистентной модели памяти (т.е. если все переменные будут volatile). Второй поток считывает vol == 0, потом первый поток присваивает обеим переменным 1, и второй поток считывает nonvol == 1.
Когда рассматривают переупорядочивания двух переменных, их всегда записывают и читают в разном порядке:
public void doIt() {
vol = 1;
nonvol = 1;
}
public void check() {
if (shared.nonvol == 1 && shared.vol == 0)
{...}
}
Такое уже невозможно (в смысле срабатывание условия) в последовательно-консистентной модели памяти (т.е. без переупорядочиваний). Т.к. если второй поток уже увидел nonvol == 1, то он должен увидеть и запись в vol.
Я подозреваю, что имелся в виду именно такой пример.
Действительно, в рамках JMM условие может сработать даже в таком коде.
Это как раз пример того, что называется Incorrectly Synchronized Programs (в предыдущем посте я назвал это non properly synchronized programs, т.к. писал по памяти, не глядя в JLS). Основной раздел, описывающий это — 17.3 Incorrectly Synchronized Programs Exhibit Surprising Behaviors.
Последовательная консистентность гарантируется только для программ, которые корректно используют volatile (ну или локи, естественно). Если программа вообще не использует volatile, или использует, но не по делу (volatile добавлен в произвольное место программы, как раз что мы имеем в данном случае), то для такой программы вообще даётся мало гарантий, она может видеть практически любые переупорядочивания (дело далеко не ограничивается только тем, что обычное сохранение может подняться за volatile сохранение).
Постараюсь привести несколько релевантных выдержек из JLS.
17.4.3 Programs and Program Order
If a program has no data races, then all executions of the program will appear to be sequentially consistent.
Это как раз то, что я пытаюсь донести — в корректной Java программе нет переупорядочиваний.
17.4.5 Happens-before Order
Programmers do not need to reason about reorderings to determine that their code contains data races. Therefore they do not need to reason about reorderings when determining whether their code is correctly
synchronized. Once the determination that the code is correctly synchronized is made, the programmer does not need to worry that reorderings will affect his or her code.
То же самое другими словами.
17.4.5 Happens-before Order
It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.
Это касательно "некорректных" программ. Реализация вольна делать вообще любые переупорядочивания пока выполняются все гарантии. А гарантии у нас есть только для корректно синхронизированных программ. Отсюда следует, что некорректно синхронизированная программа может увидеть эти самые любые переупорядочивания.
Например, были такие занятные процессоры Alpha, которые могли делать переупорядочивания зависимых по данным обращений к памяти (т.е. данные по указателю могли быть загружены до самого указателя). Это было достаточно не удобно в плане производительности, т.к. приходилось везде расставлять барьеры памяти для упорядочивания. Так там применялся следующий трюк. Переупорядочивание не подавлялось, зато после загрузки данных они проверялись на консистентность (т.е. был какой-то способ определить, что загрузили мусор). Если данные оказывались консистентны, то ничего больше делать не надо. Если нет — то уже только тут выполнялся барьер памяти и данные перечитывались.
Так вот, возвращаясь к Java. Если бы что-то типа такого было бы реализовано в JVM, то, естественно, этот дополнительный код по проверке данных и перечитываю был бы только для меж-поточных (читай — volatile) обращений. А обычные загрузки из памяти могли бы видеть полностью неадекватные данные.
Если подытожить — в корректной программе нет никаких переупорядочиваний, в некорректной — могут быть любые переупорядочивания.
Писать некорректно-синхронизированные программы (даже если вы предусмотрели все-все побочные эффекты, которые она может вызвать) как минимум дурной тон. А по-хорошему их единственное предназначение — дать хоть какие-то гарантии, что бы предотвратить компрометацию безопасности JVM.
Кстати, в Java ещё достаточно сильные гарантии касательно некорректно синхронизированных программ (с гонками). Хорошо это или плохо — это уже скорее философский вопрос. Плохие аспекты этого — компрометация производительности, люди начинают считать, что гонки это не так уж и плохо, раз для них столько гарантий.
В С++ гонки полностью запрещены, т.е. программа вольна сразу инициировать воспламенение системного блока при обнаружении хоть одной гонки (если есть соотв. переферийное устройство).
В C#/CLI некоторые гарантии есть, но они значительно хуже. В частности:
12.6.8 Other memory model issues
A conforming implementation of the CLI shall ensure that, even in a multi-threaded environment and without
proper user synchronization, objects are allocated in a manner that prevents unauthorized memory access and
prevents invalid operations from occurring. In particular, on multiprocessor memory systems where explicit
synchronization is required to ensure that all relevant data structures are visible (for example, vtable pointers)
the Execution Engine shall be responsible for either enforcing this synchronization automatically or for
converting errors due to lack of synchronization into non-fatal, non-corrupting, user-visible exceptions.
Т.е. грубо говоря, если в гонках завязаны не только простые int/long, а объекты (что скорее всего будет иметь место в реальной жизни), то при простом обращении к объекту можно "ни с того ни с сего" словить какое-то исключение (не специфицированного типа).
К чему я это всё. К тому, что я не могу не сказать, что Да, действительно, Java программа может увидеть какие-то переупорядочивания вокруг volatile. НО про это в реальной жизни лучше забыть, такая программа некорректная. В корректной Java программе никаких переупорядочиваний нет.
Здравствуйте, Golovach Ivan, Вы писали:
GI>Ну да, хаппенд-бефора тут нет. Вопрос же был — возможен ли хоть какой-то реордеринг волатайл и не-волатайл операций.
С какой точки зрения тебя интересует? С т.з. корректной Java программы или с т.з. машинного кода сгенерированного каким-то конкретным компилятором под какую-то конкретную аппаратную платформу?
Здравствуйте, Golovach Ivan, Вы писали:
GI>Согласен. Предлагаю переформулировку. Мне кажется, что переписанное условие GI>if (shared.nonvol == 1 && shared.vol == 0) {...} GI>может выполнится. По крайней мере, я не нашел запрета на это, но нашел подтверждение в виде Roach Motel Principle. GI>И если это так, что с точки зрения второго потока он будет наблюдать явление, трактуемое как реордеринг записи в vol и nonvol. Т.е. то, что категорически отвергал remark.
Такая ситуация возможна только в том случае, если приложение выполняется под таким окружением (архитектура процессора/os/jvm implementation etc), которое позволяет иметь следующую ситуацию: У класса С1 есть non-final, non-volatile поле с инициализатором;
В потоке t1 создается объект О1 класса С1;
В потоке t2 считывается О1.field, упоминавшееся в первом пункте;
t2 видит в качестве значения О1.field значение, отличное от инициализатора, заданного в классе С1;
В действительности не сталкивался с таким окружением, но фишка в том, что формально спецификация языка не запрещает такого поведения.
Опять же, изначальный пример можно немного модифицировать таким образом, чтобы в инициализаторах 'vol' и 'nonvol' были единицы, а менялись они на нули. Тогда очень даже возможна ситуация, что если ссылка на shared публикуется до конца инициализации (до того как все 'pufield' выполнились — классический пример реордеринга) , поток, производящий 'if (shared.nonvol == 0 && shared.vol == 1) {...}', видит объект в inconsistent state.
Если же считать, что мы работаем в окружении, где поток, отличный от того, в котором создан объект, всегда видит полностью инициализированный объект и может не увидеть только измененное значение поля, описанная ситуация невозможна.
Здравствуйте, denis.zhdanov, Вы писали:
DZ>Здравствуйте, Golovach Ivan, Вы писали:
GI>>Согласен. Предлагаю переформулировку. Мне кажется, что переписанное условие GI>>if (shared.nonvol == 1 && shared.vol == 0) {...} GI>>может выполнится. По крайней мере, я не нашел запрета на это, но нашел подтверждение в виде Roach Motel Principle. GI>>И если это так, что с точки зрения второго потока он будет наблюдать явление, трактуемое как реордеринг записи в vol и nonvol. Т.е. то, что категорически отвергал remark.
DZ>Такая ситуация возможна только в том случае, если приложение выполняется под таким окружением (архитектура процессора/os/jvm implementation etc), которое позволяет иметь следующую ситуацию: DZ> DZ>У класса С1 есть non-final, non-volatile поле с инициализатором; DZ>В потоке t1 создается объект О1 класса С1; DZ>В потоке t2 считывается О1.field, упоминавшееся в первом пункте; DZ>t2 видит в качестве значения О1.field значение, отличное от инициализатора, заданного в классе С1; DZ>
DZ>В действительности не сталкивался с таким окружением, но фишка в том, что формально спецификация языка не запрещает такого поведения.
DZ>Опять же, изначальный пример можно немного модифицировать таким образом, чтобы в инициализаторах 'vol' и 'nonvol' были единицы, а менялись они на нули. Тогда очень даже возможна ситуация, что если ссылка на shared публикуется до конца инициализации (до того как все 'pufield' выполнились — классический пример реордеринга) , поток, производящий 'if (shared.nonvol == 0 && shared.vol == 1) {...}', видит объект в inconsistent state.
DZ>Если же считать, что мы работаем в окружении, где поток, отличный от того, в котором создан объект, всегда видит полностью инициализированный объект и может не увидеть только измененное значение поля, описанная ситуация невозможна.
Я утвеждаю, что следующая программа МОЖЕТ передать "привет из Харькова":
class ReorderingTest {
static Shared shared = new Shared();
public static void main(String[] args) {
new Thread() {
public void run() {
shared.doIt();
}
}.start();
new Thread() {
public void run() {
shared.check();
}
}.start();
}
}
class Shared {
private volatile int vol;
private int nonvol;
public void doIt() {
this.vol = 1;
this.nonvol = 1;
}
public void check() {
if (this.nonvol == 1 && this.vol == 0) {
System.out.println("Hello from Kharkov!");
}
}
}
и что этот привет будет в согласии со спецификацией языка.
remark уже согласился, хотя и обозвал такую программу not correctly synchronized. Но! Я и не утверждал, что программа будет без дата рейсов, меня интересовала только согласованность со спекой. Т.е. перебор всех допустимых спецификацией сценариев. Если я ошибаюсь, могли бы Вы привести цитаты из спеки запрещающие привет из Харькова ?
Здравствуйте, Golovach Ivan, Вы писали:
DZ>>Если же считать, что мы работаем в окружении, где поток, отличный от того, в котором создан объект, всегда видит полностью инициализированный объект и может не увидеть только измененное значение поля, описанная ситуация невозможна.
GI>Я утвеждаю, что следующая программа МОЖЕТ передать "привет из Харькова": GI>... GI>и что этот привет будет в согласии со спецификацией языка. GI>remark уже согласился, хотя и обозвал такую программу not correctly synchronized. Но! Я и не утверждал, что программа будет без дата рейсов, меня интересовала только согласованность со спекой. Т.е. перебор всех допустимых спецификацией сценариев. Если я ошибаюсь, могли бы Вы привести цитаты из спеки запрещающие привет из Харькова ?
Давай попробуем восстановить хронологию событий:
GI>Вот программа, показывает то-то и то-то;
DZ>Такая ситуация возможна только в том случае, если приложение выполняется под таким окружением...фишка в том, что формально спецификация языка не запрещает такого поведения;
GI>Я утвеждаю, что следующая программа МОЖЕТ передать "привет из Харькова"..., если я ошибаюсь, могли бы Вы привести цитаты из спеки запрещающие привет из Харькова;
Не очень понятно, с кем ты споришь.
Здравствуйте, remark, Вы писали:
R>Я подозреваю, что имелся в виду именно такой пример.
Да, тут неправильно написал.
R>Действительно, в рамках JMM условие может сработать даже в таком коде.
Это я имел в виду. Cпека допускает reordering volatile и non-volatile. Я не утверждал, что программа будет Сorrectly Synchronized Program.
R> В C#/CLI некоторые гарантии есть, но они значительно хуже.
Спасибо. Всегда приятно, когда в Java что-то лучше чем в C#. Ничего не могу с этим поделать. Цеховая гордость.
R>НО про это в реальной жизни лучше забыть, такая программа некорректная. В корректной Java программе никаких переупорядочиваний нет.
Мне кажется я нашел источник наших противоречий. Он заключается в том, что у Вас методики применимы к Java/C#/C++, а я говорю исключительно о Java. В Java, как Вы сами говорите, очень сильные гарантии на некорректные сценарии. Собственно, я утверждаю, что Incorrectly Synchronized Programs МОЖНО писать, ИНОГДА даже НУЖНО. Я опираюсь на два факта:
1) в главе 17.3 не говорится, что Incorrectly Synchronized Programs — не надо писать. Там много фраз такого рода: "Incorrectly Synchronized Programs Exhibit Surprising Behaviors", "can produce behaviors that seem paradoxical", "Surprising results caused by ...", "It may appear that ... is impossible", "This is, on the face of it, absurd.", "To some programmers, this behavior may seem "broken"", etc. Но ни разу не говорится, что не надо такого писать.
2) как я приводил пример своим студентам [http://kharkovconcurrencygroup.blogspot.com/2009/09/0.html] data race используется SUN-ом в "свята святых" в классе java.lang.String
java.lang.String.java:
public final class String ... {
...
/** Cache the hash code for the string */private int hash; // Default to 0
...
/**
* Returns a hash code for this string. The hash code for a
* String object is computed as
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* using int arithmetic, where s[i] is the
* ith character of the string, n is the length of
* the string, and ^ indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
...
}
согласно главы 17:
"However, it should be noted that this code is improperly synchronized:
* there is a write in one thread,
* a read of the same variable by another thread,
* and the write and read are not ordered by synchronization.
This situation is an example of a data race (§17.4.5). When code contains a data race, counterintuitive results are often possible."
в классе String идет data race относительно вычисления поля int hash, но ВСЕ В СОГЛАСИИ СО СПЕКОЙ и потому сан считает такой код допустимым для оптимизации скорости.
Т.е. я утверждаю, что
1) контролируемый data race не плох. Это просто инструмент. Да утонченный, да опасный, но работающий по спеке и значит контролируемый.
2) Incorrectly Synchronized Program не является Incorrectly Program. Т.е. стоит понимать как <Incorrectly Synchronized> Program, а не как Incorrectly <Synchronized Program>. Incorrectly Synchronized не является недопустимым, это просто свойство.
Здравствуйте, denis.zhdanov, Вы писали:
DZ>Не очень понятно, с кем ты споришь.
Вы сказали "Если же считать, что мы работаем в окружении, где поток, отличный от того, в котором создан объект, всегда видит полностью инициализированный объект и может не увидеть только измененное значение поля, описанная ситуация невозможна.".
А я утверждаю что согласно спеке между потоком запустившим main (и инициализировавшим shared, и явно создавшем два других потока) и двумя созданными потоками установлено отношение happend-before (17.4.4 Synchronization Order: "An action that starts a thread synchronizes-with the first action in the thread it starts."). А значит потоки видят только полностью сконструированный объект. А значит, согласно вашему утверждению такое поведение(вывод в консоль) невозможно.
Здравствуйте, Golovach Ivan, Вы писали:
GI>Т.е. я утверждаю, что GI>1) контролируемый data race не плох. Это просто инструмент. Да утонченный, да опасный, но работающий по спеке и значит контролируемый. GI>2) Incorrectly Synchronized Program не является Incorrectly Program. Т.е. стоит понимать как <Incorrectly Synchronized> Program, а не как Incorrectly <Synchronized Program>. Incorrectly Synchronized не является недопустимым, это просто свойство.
С этим я согласен.
Спецификация языка вообще не оперирует понятиями "плохо"/"хорошо", она только вводит семантику для конструкций языка (хотя фразы типа "Incorrectly Synchronized Programs Exhibit Surprising Behaviors" видеть в спецификации несколько... необычно, появляется впечатление, что читаешь не формальную спецификацию языка, а литературное произведение, ну это так, к слову).
Т.е. если есть некий код, и согласно спецификации он делает то, что от него требуется, то собственно это и имеем, не больше и не меньше, код с таким-то поведением.
Вообще в своё время при обсуждении новой модели памяти шли дискусси о "correct but incorrectly synchronized code".
Я был не согласен с несколько другим моментом. Во-первых, с тем какие переупорядочивания возможны, во-вторых, с формой подачи этого.
Если уж говорить об этом, то ИМХО первое, что нужно говорить, так это что есть корректно- и некорректно-синхронизированный код. В целом некорректно-синхронизированный код нужно рассматривать как просто ошибочный код. И программист должен ориентироваться на корректно-синхронизированный код, для него гарантируется полная последовательная консистентность, т.е. отсутствие каких-либо переупорядочиваний.
После этого можно добавить (а можно и не добавлять), что в принципе для некорректно-синхронизированного кода предоставляются вполне определенные гарантии, которые можно использовать в некоторых случаях. НО это очень-очень специфические случаи, и вы должны быть на 200% уверены, что вы делаете.
Всё же некорректно-синхронизированный код рассматривался создателями модели памяти именно как некорректный код. Вот например цитата из "The Java Memory Model" (http://rsim.cs.illinois.edu/Pubs/popl05.pdf) (обратите внимание на выделенное):
The bulk of the effort for the revision, and our focus here,
was on understanding the requirements for incorrectly synchronized
code. The previous strategy of leaving the semantics
for incorrect programs unspecified is inconsistent with
Java’s security and safety guarantees. Such a strategy has
been used in some prior languages. For example, Ada simply
defines unsynchronized code as “erroneous” [1]. The reasoning
behind this is that since such code is incorrect (on some
level), no guarantees should be made when it occurs. This is
similar to the strategy that some languages take with array
bounds overflow – unpredictable results may occur, and it
is the programmer’s responsibility to avoid these scenarios.
The above approach does not lend itself to the writing
of secure and safe code. In an ideal world, every programmer
would write correct code all of the time. In our world,
programs frequently contain errors; not only does this cause
code to misbehave, but it can also allows attackers to violate
safety assumptions in a program (as is true with buffer
overflows). Our earlier work has described the dangers of
such scenarios in more detail [28].
Program semantics must be completely defined: if programmers
don’t know what their code is doing, they won’t
be able to know what their code is doing wrong. The second
broad requirement of the Java model is to provide a clear
and definitive semantics for how code should behave when it
is not correctly written, but without substantially affecting
current compilers and hardware.
Были даже дискуссии по поводу того, что компилятор будет отказываться компилировать программу, если он определеяет наличие некорректно-синхронизированного кода (так же как он это делает для некорректно-типизированного кода).
Здравствуйте, remark, Вы писали:
R>После этого можно добавить (а можно и не добавлять), что в принципе для некорректно-синхронизированного кода предоставляются вполне определенные гарантии, которые можно использовать в некоторых случаях. НО это очень-очень специфические случаи, и вы должны быть на 200% уверены, что вы делаете.
Единственный момент, который мне не до конца понятен касательно некорректно-синхронизированного кода, — это обеспечение когерентности.
С++ стандарт говорит "Implementations should make atomic stores visible to atomic loads within a reasonable amount of time". Т.е. этим гарантируется распространение изменений между потоками. Действия, совершённые в одном потоке, должны стать видны другим потокам в конечное время.
CLI/C# стандарт лажается в этом месте и не даёт таких гарантий. Но это я уже донёс до разработчиков, они собираются это пофиксить.
В стандарте Java я тоже вроде не вижу никаких подобных гарантий. Однако для volatile переменных можно быть уверенным, что по-крайней мере у разработчиков компиляторов есть такое намерение. А вот что касается обычных переменных это совсем не очевидно.
К чему это может привести? К тому, что изменения обычной переменной в одном потоке НИКОГДА не будут видны другим потокам (либо вследствие того, что первый поток, так и не запишет изменение в память; либо другой поток так и не будет перечитывать значение из памяти).
Если это действительно так для Java, то это ещё существенно сужает круг применимости "benign data race" ещё больше. В кода рассчёта хэша для строк это не является проблемой, т.к. всё равно остаётся гарантия, что каждый поток рассчитает хэш для строки не более одного раза. А вот если необходимо прямо или косвенно просигнализировать другой поток, то сигнал может не дойти до потока. В C# для борьбы с этим можно применять специальный аттрибут для функции, который запрещает встраивание функции, и это вроде как позволяет получить гарантию когерентности. В С++ можно просто законным способом отделить атомарность и видимость от упорядочивания. Есть ли что-то в Java для борьбы с этим — не знаю.
Здравствуйте, remark, Вы писали:
R>Я был не согласен с несколько другим моментом. Во-первых, с тем какие переупорядочивания возможны, во-вторых, с формой подачи этого. R>Если уж говорить об этом, то ИМХО первое, что нужно говорить, так это что есть корректно- и некорректно-синхронизированный код. В целом некорректно-синхронизированный код нужно рассматривать как просто ошибочный код. И программист должен ориентироваться на корректно-синхронизированный код, для него гарантируется полная последовательная консистентность, т.е. отсутствие каких-либо переупорядочиваний. R>После этого можно добавить (а можно и не добавлять), что в принципе для некорректно-синхронизированного кода предоставляются вполне определенные гарантии, которые можно использовать в некоторых случаях. НО это очень-очень специфические случаи, и вы должны быть на 200% уверены, что вы делаете.
И переупорядочивания в этом случае не сводятся только к тому, что обычные обращения могут подниматься за volatile сохранение, и опускаться через volatile загрузку.
Вот, например, хороший пример помедитировать:
volatile int V1 = 0;
volatile int V2 = 0;
int X = 0;
int Y = 0;
thread1()
{
while (V1 != 0) {}
X = 1;
V1 = 1;
while (V1 != 1) {}
Y = 1;
V1 = 2;
}
thread2()
{
while (V2 != 0) {}
int R1 = Y;
V2 = 1;
while (V2 != 1) {}
int R2 = X;
V2 = 2;
if (R1 == 1 && R2 == 0) {...}
}
Что тут переупорядочивается, когда срабатывает условие в потоке 2? Вроде как сохранения и загрузки в/из X/Y не могут никуда двигаться, т.к. сверху volatile load, а снизу — volatile store.
Здравствуйте, remark, Вы писали:
R>Что тут переупорядочивается, когда срабатывает условие в потоке 2? Вроде как сохранения и загрузки в/из X/Y не могут никуда двигаться, т.к. сверху volatile load, а снизу — volatile store.
R>
Блин. Честно прочитал все ветку. Не понял одного. Кто о чем и с кем спорит? Тоесть приводят куски кода и говорят: "Истину вам говорю, 4 мая 1925 года Земля налетит на небесную осью". Тоесть, никто из ораторов даже не сказал а как на самом деле работат приведенный код.
Вот и сейчас. Лично мне кажется что этот код корректный. И никакого реордеринга быть не должно, исходя из спеки. И все будет хорошо. И все поженятся. Или у тебя есть печальный опыт что подобное уловие срабатывало?
Здравствуйте, Nicht, Вы писали:
R>>Что тут переупорядочивается, когда срабатывает условие в потоке 2? Вроде как сохранения и загрузки в/из X/Y не могут никуда двигаться, т.к. сверху volatile load, а снизу — volatile store.
N>Блин. Честно прочитал все ветку. Не понял одного. Кто о чем и с кем спорит? Тоесть приводят куски кода и говорят: "Истину вам говорю, 4 мая 1925 года Земля налетит на небесную осью". Тоесть, никто из ораторов даже не сказал а как на самом деле работат приведенный код. N>Вот и сейчас. Лично мне кажется что этот код корректный. И никакого реордеринга быть не должно, исходя из спеки. И все будет хорошо. И все поженятся. Или у тебя есть печальный опыт что подобное уловие срабатывало?
Если дело касается портабельного кода, то я предпочитаю не полагаться на опыт и на то, что что-то где-то работает или не работает. То, что действительно имеет значение, это — спецификация языка.
Согласно спецификации сохранение в Х НЕ happens-before загрузки из Х, даже если загрузка из Y вернула 1. Следовательно загрузка из Х может вернуть или 1 или начальное значение 0.
Здравствуйте, Nicht, Вы писали:
N>Блин. Честно прочитал все ветку. Не понял одного. Кто о чем и с кем спорит?
Это не спор, это дискуссия.
Мы пытаемся найти истину. Как-оно-все-работает-на-самом-деле.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, remark, Вы писали:
R>>После этого можно добавить (а можно и не добавлять), что в принципе для некорректно-синхронизированного кода предоставляются вполне определенные гарантии, которые можно использовать в некоторых случаях. НО это очень-очень специфические случаи, и вы должны быть на 200% уверены, что вы делаете.
R>Единственный момент, который мне не до конца понятен касательно некорректно-синхронизированного кода, — это обеспечение когерентности. R>С++ стандарт говорит "Implementations should make atomic stores visible to atomic loads within a reasonable amount of time". Т.е. этим гарантируется распространение изменений между потоками. Действия, совершённые в одном потоке, должны стать видны другим потокам в конечное время. R>CLI/C# стандарт лажается в этом месте и не даёт таких гарантий. Но это я уже донёс до разработчиков, они собираются это пофиксить. R>В стандарте Java я тоже вроде не вижу никаких подобных гарантий. Однако для volatile переменных можно быть уверенным, что по-крайней мере у разработчиков компиляторов есть такое намерение.
Для обычных ничего не нашел. Детальный анализ гарантий для volatile немного загнал меня в тупик:
JVM Spec. Edition 2. CHAPTER 8. Threads and Locks.[http://java.sun.com/docs/books/jvms/second_edition/html/Threads.doc.html]:
"Variables are kept in a main memory that is shared by all threads."
"Every thread has a working memory in which it keeps its own working copy of variables that it must use or assign. As the thread executes a program, it operates on these working copies. The main memory contains the master copy of every variable. There are rules about when a thread is permitted or required to transfer the contents of its working copy of a variable into the master copy or vice versa."
"A use or assign operation is a tightly coupled interaction between a thread's execution engine and the thread's working memory."
"When data is copied from the main memory to a working memory, two actions must occur: a read operation performed by the main memory, followed some time later by a corresponding load operation performed by the working memory. When data is copied from a working memory to the main memory, two actions must occur: a store operation performed by the working memory, followed some time later by a corresponding write operation performed by the main memory. There may be some transit time between main memory and a working memory, and the transit time may be different for each transaction;"
8.7 Rules for volatile Variables:
"# A use operation by T on V is permitted only if the previous operation by T on V was load, and a load operation by T on V is permitted only if the next operation by T on V is use. The use operation is said to be "associated" with the read operation that corresponds to the load.
# A store operation by T on V is permitted only if the previous operation by T on V was assign, and an assign operation by T on V is permitted only if the next operation by T on V is store. The assign operation is said to be "associated" with the write operation that corresponds to the store."
Т.е. выходит, присвоение для volatile переменной соответствует выполняемым _в_этот_момент_ инструкциям assign+store, но вот инструкция write (to main memory) происходит !!!some time later!!!. Т.е. выходит, что в коде присвоение volatile переменной уже произошло и поток пошел дальше, но в main memory данное не установлено.
Я полагал, что assign, store и write выполняются вместе. Спасибо, Вы сподвигли меня на исследования JVM Spec.
R>А вот что касается обычных переменных это совсем не очевидно. К чему это может привести? К тому, что изменения обычной переменной в одном потоке НИКОГДА не будут видны другим потокам (либо вследствие того, что первый поток, так и не запишет изменение в память; либо другой поток так и не будет перечитывать значение из памяти).
Согласен. Классический пример приведен в книге Effective Java. Second Edition. Joshua Bloch:
Item 66: Synchronize access to shared mutable data:
"
// Broken! - How long would you expect this program to run?public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
You might expect this program to run for about a second, after which the main
thread sets stopRequested to true, causing the background thread’s loop to terminate.
On my machine, however, the program never terminates: the background
thread loops forever!"
Джошуа предлагает интерпретацию в виде единичного считывания флага вторым потоком
"This optimization is known as hoisting, and it is precisely what the HotSpot server
VM does."
R>Если это действительно так для Java, то это ещё существенно сужает круг применимости "benign data race" ещё больше. В кода рассчёта хэша для строк это не является проблемой, т.к. всё равно остаётся гарантия, что каждый поток рассчитает хэш для строки не более одного раза. А вот если необходимо прямо или косвенно просигнализировать другой поток, то сигнал может не дойти до потока. В C# для борьбы с этим можно применять специальный аттрибут для функции, который запрещает встраивание функции, и это вроде как позволяет получить гарантию когерентности. В С++ можно просто законным способом отделить атомарность и видимость от упорядочивания. Есть ли что-то в Java для борьбы с этим — не знаю.
Все что я знаю "промежуточное" между volatile и non-volatile переменными с точки зрения когерентности кэша это java.util.concurrent.atomic.Atomic*.lazySet(...) [http://java.sun.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html]:
"lazySet has the memory effects of writing (assigning) a volatile variable except that it permits reorderings with subsequent (but not previous) memory actions that do not themselves impose reordering constraints with ordinary non-volatile writes. Among other usage contexts, lazySet may apply when nulling out, for the sake of garbage collection, a reference that is never accessed again."
[http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6275329]:
"As probably the last little JSR166 follow-up for Mustang,
we added a "lazySet" method to the Atomic classes
(AtomicInteger, AtomicReference, etc). This is a niche
method that is sometimes useful when fine-tuning code using
non-blocking data structures. The semantics are
that the write is guaranteed not to be re-ordered with any
previous write, but may be reordered with subsequent operations
(or equivalently, might not be visible to other threads) until
some other volatile write or synchronizing action occurs).
The main use case is for nulling out fields of nodes in
non-blocking data structures solely for the sake of avoiding
long-term garbage retention; it applies when it is harmless
if other threads see non-null values for a while, but you'd
like to ensure that structures are eventually GCable. In such
cases, you can get better performance by avoiding
the costs of the null volatile-write. There are a few
other use cases along these lines for non-reference-based
atomics as well, so the method is supported across all of the
AtomicX classes.
For people who like to think of these operations in terms of
machine-level barriers on common multiprocessors, lazySet
provides a preceeding store-store barrier (which is either
a no-op or very cheap on current platforms), but no
store-load barrier (which is usually the expensive part
of a volatile-write)."
P.S. Добавили в java 1.6. Возможно Вы не сталкивались.
Здравствуйте, Golovach Ivan, Вы писали:
R>>Единственный момент, который мне не до конца понятен касательно некорректно-синхронизированного кода, — это обеспечение когерентности. R>>С++ стандарт говорит "Implementations should make atomic stores visible to atomic loads within a reasonable amount of time". Т.е. этим гарантируется распространение изменений между потоками. Действия, совершённые в одном потоке, должны стать видны другим потокам в конечное время. R>>CLI/C# стандарт лажается в этом месте и не даёт таких гарантий. Но это я уже донёс до разработчиков, они собираются это пофиксить. R>>В стандарте Java я тоже вроде не вижу никаких подобных гарантий. Однако для volatile переменных можно быть уверенным, что по-крайней мере у разработчиков компиляторов есть такое намерение.
GI>Для обычных ничего не нашел. Детальный анализ гарантий для volatile немного загнал меня в тупик:
Это нормально
GI>JVM Spec. Edition 2. CHAPTER 8. Threads and Locks.[http://java.sun.com/docs/books/jvms/second_edition/html/Threads.doc.html]: GI>"Variables are kept in a main memory that is shared by all threads." GI>"Every thread has a working memory in which it keeps its own working copy of variables that it must use or assign. As the thread executes a program, it operates on these working copies. The main memory contains the master copy of every variable. There are rules about when a thread is permitted or required to transfer the contents of its working copy of a variable into the master copy or vice versa." GI>"A use or assign operation is a tightly coupled interaction between a thread's execution engine and the thread's working memory." GI>"When data is copied from the main memory to a working memory, two actions must occur: a read operation performed by the main memory, followed some time later by a corresponding load operation performed by the working memory. When data is copied from a working memory to the main memory, two actions must occur: a store operation performed by the working memory, followed some time later by a corresponding write operation performed by the main memory. There may be some transit time between main memory and a working memory, and the transit time may be different for each transaction;" GI>8.7 Rules for volatile Variables: GI>"# A use operation by T on V is permitted only if the previous operation by T on V was load, and a load operation by T on V is permitted only if the next operation by T on V is use. The use operation is said to be "associated" with the read operation that corresponds to the load. GI># A store operation by T on V is permitted only if the previous operation by T on V was assign, and an assign operation by T on V is permitted only if the next operation by T on V is store. The assign operation is said to be "associated" with the write operation that corresponds to the store." GI>Т.е. выходит, присвоение для volatile переменной соответствует выполняемым _в_этот_момент_ инструкциям assign+store, но вот инструкция write (to main memory) происходит !!!some time later!!!. Т.е. выходит, что в коде присвоение volatile переменной уже произошло и поток пошел дальше, но в main memory данное не установлено.
Видимо write действительно может быть выполнен позже (собственно на практике так оно и происходит), НО для volatile в силе дополнительные гарантии, которые обусловлены happens-before отношениями. А покуда они выполняются, отложенная запись не может вызвать проблем.
GI>Я полагал, что assign, store и write выполняются вместе. Спасибо, Вы сподвигли меня на исследования JVM Spec.
R>>А вот что касается обычных переменных это совсем не очевидно. К чему это может привести? К тому, что изменения обычной переменной в одном потоке НИКОГДА не будут видны другим потокам (либо вследствие того, что первый поток, так и не запишет изменение в память; либо другой поток так и не будет перечитывать значение из памяти).
GI>Согласен. Классический пример приведен в книге Effective Java. Second Edition. Joshua Bloch: GI>Item 66: Synchronize access to shared mutable data: GI>"
// Broken! - How long would you expect this program to run?
GI>public class StopThread {
GI> private static boolean stopRequested;
GI> public static void main(String[] args) throws InterruptedException {
GI> Thread backgroundThread = new Thread(new Runnable() {
GI> public void run() {
GI> int i = 0;
GI> while (!stopRequested)
GI> i++;
GI> }
GI> });
GI> backgroundThread.start();
GI> TimeUnit.SECONDS.sleep(1);
GI> stopRequested = true;
GI> }
GI>}
GI>You might expect this program to run for about a second, after which the main GI>thread sets stopRequested to true, causing the background thread’s loop to terminate. GI>On my machine, however, the program never terminates: the background GI>thread loops forever!" GI>Джошуа предлагает интерпретацию в виде единичного считывания флага вторым потоком GI>"This optimization is known as hoisting, and it is precisely what the HotSpot server GI>VM does."
Понятно.
Это существенно ограничивает область применимости обычных переменных для синхронизации. Большинство алгоритмов синхронизации (такие как мьютексы, очереди, стеки и т.д.) рассчитывают на гарантию когерентности, т.е. на распространение изменений в конечном счёте, и без неё работать не будет.
R>>Если это действительно так для Java, то это ещё существенно сужает круг применимости "benign data race" ещё больше. В кода рассчёта хэша для строк это не является проблемой, т.к. всё равно остаётся гарантия, что каждый поток рассчитает хэш для строки не более одного раза. А вот если необходимо прямо или косвенно просигнализировать другой поток, то сигнал может не дойти до потока. В C# для борьбы с этим можно применять специальный аттрибут для функции, который запрещает встраивание функции, и это вроде как позволяет получить гарантию когерентности. В С++ можно просто законным способом отделить атомарность и видимость от упорядочивания. Есть ли что-то в Java для борьбы с этим — не знаю. GI>Все что я знаю "промежуточное" между volatile и non-volatile переменными с точки зрения когерентности кэша это java.util.concurrent.atomic.Atomic*.lazySet(...) [http://java.sun.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html]: GI>"lazySet has the memory effects of writing (assigning) a volatile variable except that it permits reorderings with subsequent (but not previous) memory actions that do not themselves impose reordering constraints with ordinary non-volatile writes. Among other usage contexts, lazySet may apply when nulling out, for the sake of garbage collection, a reference that is never accessed again."
GI>P.S. Добавили в java 1.6. Возможно Вы не сталкивались.
Занятно.
Не сталкивался раньше... ну хотя я вообще с Java сталкиваюсь только "постольку-поскольку".
Документация, как всегда для таких вопросов, оставляет желать лучшего. Не понятно, последующие операции могут подниматься вверх за lazySet() за счёт переупорядочиваний процессором или и компилятором. Из документации вроде как следует, что и компилятором, и тогда lazySet() получается практически таким же бесполезным как и обычные переменные, т.к. отсутствует гарантия когерентности. Хотя из здравого смысла разумно предположить, что намерение было допускать только переупорядочивания процессором, и тогда это — вполне полезная функция.
В смысле полезная до известного предела:
low-level atomics — the programming equivalent of playing Russian Roulette with all chambers loaded
В нём есть методы compareAndSwapXXX(), которые не дают никаких гарантий упорядочивания, и соотв. значительно быстрее обычного Atomic.compareAndSwap() на некоторых архитектурах. Они используются в нашумевшем hash map от Cliff Click.
Есть putOrderedХХХ(), которые видимо есть аналог lazySet().
Есть putХХХVolatile(), которые аналог volatile store (но его соотв. можно применять избирательно).
И есть putXXX(), которое видимо и есть то самое заветное "обычное сохранение, но с гарантией когерентности".
Кстати, если lazySet() реализован через Unsafe.putOrderedХХХ(), то он тоже должен предоставлять гарантию когерентности.
И соотв. есть аналоги для get, через которые можно получить "обычную загрузку, но с гарантией когерентности".
Хотя это всё есть "unsafe" и не портируемое, однако насколько я понимаю поддерживается не только компилятором Sun, собственно ссылка, которую я привёл, относится к gcj.
И так же на подходе Fences API: http://gee.cs.oswego.edu/dl/jsr166/dist/docs/java/util/concurrent/atomic/Fences.html
Fences API будет позволять делать избирательную fine-grained синхронизацию в необходимых местах.
Хммм... хотя тут тоже для меня не понятно с когерентностью... вроде никаких явных гарантий нет... видимо я всё же упускаю что-то важное для Java
Здравствуйте, remark, Вы писали:
R>Здравствуйте, remark, Вы писали:
R>>Я был не согласен с несколько другим моментом. Во-первых, с тем какие переупорядочивания возможны, во-вторых, с формой подачи этого. R>>Если уж говорить об этом, то ИМХО первое, что нужно говорить, так это что есть корректно- и некорректно-синхронизированный код. В целом некорректно-синхронизированный код нужно рассматривать как просто ошибочный код. И программист должен ориентироваться на корректно-синхронизированный код, для него гарантируется полная последовательная консистентность, т.е. отсутствие каких-либо переупорядочиваний. R>>После этого можно добавить (а можно и не добавлять), что в принципе для некорректно-синхронизированного кода предоставляются вполне определенные гарантии, которые можно использовать в некоторых случаях. НО это очень-очень специфические случаи, и вы должны быть на 200% уверены, что вы делаете.
R>И переупорядочивания в этом случае не сводятся только к тому, что обычные обращения могут подниматься за volatile сохранение, и опускаться через volatile загрузку. R>Вот, например, хороший пример помедитировать:
R>
R>volatile int V1 = 0;
R>volatile int V2 = 0;
R>int X = 0;
R>int Y = 0;
R>thread1()
R>{
R> while (V1 != 0) {}
R> X = 1;
R> V1 = 1;
R> while (V1 != 1) {}
R> Y = 1;
R> V1 = 2;
R>}
R>thread2()
R>{
R> while (V2 != 0) {}
R> int R1 = Y;
R> V2 = 1;
R> while (V2 != 1) {}
R> int R2 = X;
R> V2 = 2;
R> if (R1 == 1 && R2 == 0) {...}
R>}
R>
R>Что тут переупорядочивается, когда срабатывает условие в потоке 2? Вроде как сохранения и загрузки в/из X/Y не могут никуда двигаться, т.к. сверху volatile load, а снизу — volatile store.
По моему мнению, тут небольшое недоразумение. В отличии от моделей памяти процессоров, где (как я понимаю) инструкции LFENCE, SFENCE, MFENCE, locking instructions, etc. имеют ordering-свойства НЕ ЗАВИСЯЩИЕ ОТ АДРЕССА в памяти, в JMM happend-before (и связанные с ним ordering-гарантии) происходит ТОЛЬКО ПРИ ОБРАЩЕНИИ К ОДНОМУ ДАННОМУ:
JLS.ver3: Chapter 17. Threads and locks. [http://java.sun.com/docs/books/jls/third_edition/html/memory.html]
"17.4.4 Synchronization Order:
A write to a volatile variable (§8.3.1.4) v synchronizes-with all subsequent reads of v by any thread (where subsequent is defined according to the synchronization order)."
"17.4.5 Happens-before Order:
A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field."
И так как поток thread1 работает исключительно с V1, а поток thread2 работает исключительно с V2, то между ними не устанавливается отношение happend-before и, как следствие, во втором потоке может выполниться любое условие на декартовом произведении всех переменных со всеми значениями:
if (R1 == 0 && R2 == 0) {...}
if (R1 == 0 && R2 == 1) {...}
if (R1 == 1 && R2 == 0) {...}
if (R1 == 1 && R2 == 1) {...}
Для наглядности, достаточно предположить, что первый поток уже отработал и в его локальной памяти лежат X=1, Y=1, у второго потока изначально в локальной памяти лежало X=0, Y=0. Раз нет happend-before со вторым потоком, то локальная память второго потока не обязана "получить" значения X=1, Y=1, но может. И, как следствие, второй поток может увидеть любую комбинацию из того что есть у него и того что есть у первого потока.
Здравствуйте, Golovach Ivan, Вы писали:
GI>По моему мнению, тут небольшое недоразумение. В отличии от моделей памяти процессоров, где (как я понимаю) инструкции LFENCE, SFENCE, MFENCE, locking instructions, etc. имеют ordering-свойства НЕ ЗАВИСЯЩИЕ ОТ АДРЕССА в памяти, в JMM happend-before (и связанные с ним ordering-гарантии) происходит ТОЛЬКО ПРИ ОБРАЩЕНИИ К ОДНОМУ ДАННОМУ:
Я хотел показать, что суть не в этом и это вообще не то, как нужно думать, и вообще говоря, могут переупорядочиваться (обычное обращение и volatile store) и/или (volatile load и обычное обращение). Другого способа объяснить результат в моём примере я не вижу.
Здравствуйте, Golovach Ivan, Вы писали:
GI>По моему мнению, тут небольшое недоразумение. В отличии от моделей памяти процессоров, где (как я понимаю) инструкции LFENCE, SFENCE, MFENCE, locking instructions, etc. имеют ordering-свойства НЕ ЗАВИСЯЩИЕ ОТ АДРЕССА в памяти
Есть разные модели. Например на IA-64 упорядочивание обеспечивается командами ld.acq/st.rel (load-acquire/store-release), что фактически соответствует современной модели принятой в C/C++/C#/Java.
Здравствуйте, Golovach Ivan, Вы писали:
GI>Вы сказали "Если же считать, что мы работаем в окружении, где поток, отличный от того, в котором создан объект, всегда видит полностью инициализированный объект и может не увидеть только измененное значение поля, описанная ситуация невозможна.". GI>А я утверждаю что согласно спеке между потоком запустившим main (и инициализировавшим shared, и явно создавшем два других потока) и двумя созданными потоками установлено отношение happend-before (17.4.4 Synchronization Order: "An action that starts a thread synchronizes-with the first action in the thread it starts."). А значит потоки видят только полностью сконструированный объект. А значит, согласно вашему утверждению такое поведение(вывод в консоль) невозможно.
Здравствуйте, Golovach Ivan, Вы писали:
GI>Для обычных ничего не нашел. Детальный анализ гарантий для volatile немного загнал меня в тупик: GI>JVM Spec. Edition 2. CHAPTER 8. Threads and Locks.[http://java.sun.com/docs/books/jvms/second_edition/html/Threads.doc.html]: GI>"Variables are kept in a main memory that is shared by all threads." GI>...
Некорректно рассматривать старую спецификацию. Правильная лежит здесь.
Здравствуйте, denis.zhdanov, Вы писали:
DZ>Здравствуйте, Golovach Ivan, Вы писали:
GI>>Для обычных ничего не нашел. Детальный анализ гарантий для volatile немного загнал меня в тупик: GI>>JVM Spec. Edition 2. CHAPTER 8. Threads and Locks.[http://java.sun.com/docs/books/jvms/second_edition/html/Threads.doc.html]: GI>>"Variables are kept in a main memory that is shared by all threads." GI>>...
DZ>Некорректно рассматривать старую спецификацию. Правильная лежит здесь.
Некорректно давать ссылку на спецификацию JLS, когда речь шла о спецификации JVM
Здравствуйте, denis.zhdanov, Вы писали:
DZ>Здравствуйте, remark, Вы писали:
R>>Некорректно давать ссылку на спецификацию JLS, когда речь шла о спецификации JVM
R>>
DZ>Точно!
Согласен, мое цитирование некорректно.
Версия JVM Spec Ver2 уже устарела, а в третьей еще не написаны Самая Главная Глава №8: "Threads and Locks".
Вместо нее там стоит отсылка
"IN The Java™ Virtual Machine Specification, Second Edition, Chapter 8 detailed
the low-level actions that explained the interaction of Java virtual machine threads
with a shared main memory. It was adapted from Chapter 17 of The Java™ Language
Specification, First Edition.
Chapter 17 in The Java™ Language Specification, Third Edition, was updated
to incorporate The Java™ Memory Model and Thread Specification produced by
the JSR-133 Expert Group. The reader is referred to that chapter for information
about threads and locks."
P.S. С этим New JMM — вообще огромная проблема, нет единственного полного официального описания от SUN. Приходится опираться на статьи по всей сети непосредственно от авторов.
P.P.S. JLS Spec ver3 — не цитировал, так как там нет достаточного материала по volatile переменным.