Аннотация:
Книга будет полезна в первую очередь для 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."). А значит потоки видят только полностью сконструированный объект. А значит, согласно вашему утверждению такое поведение(вывод в консоль) невозможно.