Здравствуйте, aefimov, Вы писали:
A>Здравствуйте, korostoff, Вы писали:
A>Щас наверное меня побьют тут старшие товарисчи, но лучше я буду выглядеть балбесом, чем просто не буду знать, что происходит на самом деле. A>Вообщем так: A>Нет никакой work memmory. Это скажем так, выдумки (а может и не выдумки, а так у них просто реализовано) IBM.
Посмотрел в JLS 3.0 — там нет такого термина. В предыдущей JLS точно был, зуб даю .
A>Я понимаю, что мне сейчас скажут про кеши процессора, если этот код будет выполнять на многопроцессорной тачке. Но, в JLS по этому поводу ничего не сказано.
Опять же, в JLS 3.0 все это написано абстрактно (без ссылок на причины проблем), они ввели новое понятие "Happens Before Ordering" и пляшут от него, в предудущей было написано про кеши и т.д.
Но касательно твоего примера
A>
A>private int i;
A>public void thread1() {
A> int t = i++;
A> System.out.println("1:" + t);
A>}
A>public void thread2() {
A> int t = i++;
A> System.out.println("1:" + t);
A>}
A>
в JLS 3.0 есть такие стороки
When a program contains two conflicting accesses (§17.4.1) that are not ordered
by a happens-before relationship, it is said to contain a data race.
А в примере как раз не соблюдается "happens before" принцип т.к. не выполняется ни одно из условий Happens Before Order (см. 17.4.5) в частности вот это самое интересное
If an action x synchronizes-with a following action y, then we also have hb(x, y).
Соответственно следуя спеке в этом коде data race. И причины его, как решили авторы спеки, не обязательно растокловавасть
Здравствуйте, aefimov, Вы писали:
A>Т.е. теперь я постараюсь объяснить почему volatile на i++ не нужен и операция является thread-safe, как и остальные атомарные действия.
A>Все, бейте!
Думаю, что неатомарность обычного инкремента может доказать следующий эксперимент.
Давайте попробуем из нескольких потоков вызывать инкремент shared переменной. Если инкремент атомарен, то эта переменная увеличится на число равное количеству потоков умноженное на количество раз, на которое каждый поток попытался ее увеличить. Если инкремент неатомарен, то возможны "пропуски" и переменная увеличится на меньшее число.
public class Main implements Runnable {
private static final int ITERATIONS = 100000;
private static final int THREADS = 10;
private/*volatile*/int _counter = 0;
private boolean _start = false;
public static void main(String[] args) {
Main main = new Main();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; ++i) {
threads[i] = new Thread(main);
threads[i].start();
}
main.go();
for (int i = 0; i < THREADS; ++i) {
try {
threads[i].join();
} catch (InterruptedException exc) {
throw new RuntimeException(exc);
}
}
System.out.println("actual : " + main._counter);
System.out.println("expected : " + (ITERATIONS * THREADS));
System.out.println("passed : " + (main._counter == (ITERATIONS * THREADS)));
}
public void run() {
barrier();
for (int i = 0; i < ITERATIONS; ++i) {
// synchronized(this) {
++_counter;
// }
}
}
private synchronized void barrier() {
while (!_start) {
try {
wait();
} catch (InterruptedException exc) {
throw new RuntimeException(exc);
}
}
}
private synchronized void go() {
_start = true;
notifyAll();
}
}
Здравствуйте, aefimov, Вы писали:
A>Т.е. теперь я постараюсь объяснить почему volatile на i++ не нужен и операция является thread-safe, как и остальные атомарные действия.
volatile на i++ не нужен, но только потому, что он там не поможет.
A>В этом случае невозможно получить output с двумя одинаковыми значениями, volatile тут не нужен.
Практика — критерий истины. Я запустил твой прогу на пеньке с HT. Прогу несколько модифицировал:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ThreadTest {
private final ConcurrentHashMap<Integer, Integer> m = new ConcurrentHashMap<Integer, Integer>();
private int i;
public int thread1() {
return i++;
}
public int thread2() {
return i++;
}
public static void main(String[] args) {
final Random r = new Random();
final ThreadTest t = new ThreadTest();
Thread t1 = new Thread() {
public void run() {
for (; ;) {
int count = t.thread1();
Integer has = t.m.putIfAbsent(count, 1);
if (has != null) System.out.println("1 ("+has+") "+count);
}
}
};
t1.start();
Thread t2 = new Thread() {
public void run() {
for (; ;) {
t.thread2();
int count = t.thread1();
Integer has = t.m.putIfAbsent(count, 2);
if (has != null) System.out.println("2 ("+has+") "+count);
}
}
};
t2.start();
try {
t1.join();
t2.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
То есть она запоминает каждое возвращаемое значение. И если уже такое есть, то его выводит. Вот результат без volatile:
Здравствуйте, dshe, Вы писали:
D>Думаю, что неатомарность обычного инкремента может доказать следующий эксперимент.
Нужно доказать, что атомарное действие может быть не thread-safe
D>Давайте попробуем из нескольких потоков вызывать инкремент shared переменной. Если инкремент атомарен, то эта переменная увеличится на число равное количеству потоков умноженное на количество раз, на которое каждый поток попытался ее увеличить. Если инкремент неатомарен, то возможны "пропуски" и переменная увеличится на меньшее число. D>У меня получилось (как и ожидалось): D>
Ваш пример у меня отрабатывает ровно, но при увеличении числа тредов начинает действительно показывать разные результаты. Я ниже объясню почему так происходит, с моей точки зрения.
Все-же я хочу сказать, что любая операция, атомарная, всегда безопасна, но как доказать это на примере я не знаю. Я подразумеваю под thread-safe следующее:
int a = b++;
Есть три состояния, "до выполнения", "выполнение" и "после выполнения". Я утверждаю, что при том же инкременте сценарий такой:
{не thread-safe} добрались до инкремента
{thread-safe!} -> до выполнения: at = a, bt = b
{thread-safe!} -> выполение: at = bt + 1
{thread-safe!} -> после выполения: a = at, b = bt
{не thread-safe} выбрались из инкремента, вот тут значение a уже может быть каким угодно (даже если a объявлена как volatile)
Еще пример:
int c = a + b;
int d = a + b;
Тут две операции и обе атомарны. Когда вычисляется c — я уверен (JVM это гарантирует), что в момент вычисления ни a ни b не изменятся. Т.е. если в операцию пришло 2 и 3, то значит c будет 5. Но, при этом же, после выполнения c = a + b, JVM может переключится на другую операцию, вместо того чтобы вычислить d = a + b (допустим, что JVM не догадалось использовать результат . И поэтому, в момент вычисления d = a + b, сами a и b могут уже быть другими. В результате возможна ситуация, когда c != d. И все это, только лишь потому, что тут не одна атомарная операция, а две.
Вернемся к примерам, дело в том, что это не совсем "честный" тест. Особенно, что касается циклов. JVM просто соптимизировала их. Суть теста должна заключаться в том, чтобы столкнуть треды на этом инкременте, цикл для этого не подходит. Надо каждый раз создавать пучок тредов и пускать их одновременно (notifyAll). Ниже измененный тест, он отрабатывает не мгновенно, как ваш, т.е. тут JVM уже не смогла ничего оптимизировать.
public class Main implements Runnable {
private static final int ITERATIONS = 1000;
private static final int THREADS = 100;
private/*volatile*/int counter = 0;
public static void main(String[] args) {
Main main = new Main();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(main);
threads[i].start();
}
for (int i = 0; i < ITERATIONS; i++) {
int old = main.counter;
// Wait until all threads deadfor (Thread thread = getActiveThread(threads); thread != null; thread = getActiveThread(threads)) {
synchronized (main) {
// Release waiting threads
main.notifyAll();
}
}
// Start again (keep waiting)if (i < ITERATIONS - 1) {
for (int j = 0; j < threads.length; j++) {
threads[j] = new Thread(main);
threads[j].start();
}
}
if (old + THREADS != main.counter) {
System.out.println("fault on iteration " + i + ": " + (main.counter - old) + " increments missed");
}
}
System.out.println("actual : " + main.counter);
System.out.println("expected : " + (ITERATIONS * THREADS));
System.out.println("passed : " + (main.counter == (ITERATIONS * THREADS)));
}
private static Thread getActiveThread(Thread[] threads) {
for (int i = 0; i < threads.length; i++) {
Thread thread = threads[i];
if (thread.isAlive()) {
return thread;
}
}
return null;
}
public void run() {
freeze();
counter++;
}
private synchronized void freeze() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Также и с другим примером, который привели с AtomicInteger — там идет вызов к функции, в недрах Unsafe, и JVM не может такое оптимизировать. Поэтому, я грешу на то, что тесты показывают такие результаты именно из-за оптимизатора. Но я как-то задал 1000 тредов на 1000 итераций в этом тесте и после десяти минут ожидания оно таки выдало, что "fault on iteration 378: 177 increments missed", правда при этом у меня комп просто умер и IDE долго не откликалось, возможно — это наведенные помехи
Здравствуйте, dshe, Вы писали:
D>Думаю, что неатомарность обычного инкремента может доказать следующий эксперимент. D>Давайте попробуем из нескольких потоков вызывать инкремент shared переменной. Если инкремент атомарен, то эта переменная увеличится на число равное количеству потоков умноженное на количество раз, на которое каждый поток попытался ее увеличить. Если инкремент неатомарен, то возможны "пропуски" и переменная увеличится на меньшее число.
Извиняюсь, вчерашний пример был с ошибкой. Там двойная синхронизация была, в результате отпускался только один трет с каждым notifyAll, т.е. data race не было. Вот это правильный пример, тут они сталкиваются лбами
public class Main implements Runnable {
private static final int ITERATIONS = 100;
private static final int THREADS = 100;
private/*volatile*/int counter = 0;
private boolean keepWaiting = true;
public static void main(String[] args) {
Main main = new Main();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(main);
threads[i].start();
}
for (int i = 0; i < ITERATIONS; i++) {
int old = main.counter;
synchronized (main) {
// Release waiting threads
main.keepWaiting = false;
main.notifyAll();
}
// Wait until all threads deadfor (Thread thread = getActiveThread(threads); thread != null; thread = getActiveThread(threads)) {
synchronized (main) {
try {
System.out.println("waiting threads on iteration " + i + ": active threads - " + Thread.activeCount());
main.wait(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized (main) {
main.keepWaiting = true;
// Start again (keep waiting)if (i < ITERATIONS - 1) {
for (int j = 0; j < threads.length; j++) {
threads[j] = new Thread(main);
threads[j].start();
}
}
}
if (old + THREADS != main.counter) {
System.out.println("fault on iteration " + i + ": " + (main.counter - old) + " increments missed");
}
}
System.out.println("actual : " + main.counter);
System.out.println("expected : " + (ITERATIONS * THREADS));
System.out.println("passed : " + (main.counter == (ITERATIONS * THREADS)));
}
private static Thread getActiveThread(Thread[] threads) {
for (int i = 0; i < threads.length; i++) {
Thread thread = threads[i];
if (thread.isAlive()) {
return thread;
}
}
return null;
}
public void run() {
synchronized (this) {
try {
if (keepWaiting) {
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
counter++;
}
}
Atomicity guarantees ensure that when a non-long/double field is used in an expression, you will obtain either its initial value or some value that was written by some thread, but not some jumble of bits resulting from two or more threads both trying to write values at the same time. However, as seen below, atomicity alone does not guarantee that you will get the value most recently written by any thread. For this reason, atomicity guarantees per se normally have little impact on concurrent program design.
Volatile
In terms of atomicity, visibility, and ordering, declaring a field as volatile is nearly identical in effect to using a little fully synchronized class protecting only that field via get/set methods, as in:
final class VFloat {
private float value;
final synchronized void set(float f) { value = f; }
final synchronized float get() { return value; }
}
Declaring a field as volatile differs only in that no locking is involved. In particular, composite read/write operations such as the "++'' operation on volatile variables are not performed atomically.
Кстати, твой пример (второй), естественно, тоже не проходит. Если он у тебя проходит, то вполне возможно, что там просто однопроцесорная машина (и без HT).
Здравствуйте, Andrei N.Sobchuck, Вы писали:
ANS>Declaring a field as volatile differs only in that no locking is involved. In particular, composite read/write operations such as the "++'' operation on volatile variables are not performed atomically. ANS>
Спасибо, почитаю. То, что инкремент не атомарен, это конечно — открытие для меня...
ANS>Кстати, твой пример (второй), естественно, тоже не проходит. Если он у тебя проходит, то вполне возможно, что там просто однопроцесорная машина (и без HT).
Угу, без HT. На HT оно начинает бодаться? Проверить бы еще на полноценных двух-бошковых...
Здравствуйте, aefimov, Вы писали:
ANS>>Кстати, твой пример (второй), естественно, тоже не проходит. Если он у тебя проходит, то вполне возможно, что там просто однопроцесорная машина (и без HT).
A>Угу, без HT. На HT оно начинает бодаться? Проверить бы еще на полноценных двух-бошковых...
Тоже н епроходит Проверил толи на 4-х головом пеньке, толи на 2-х головом но с HT.
Я так понимаю, что на однопроцесорной машине всё таки работа идёт последовательно, и вероятность, что тред остановится именно посередине инкремента небольшая. Плюс, если не будет volatile, то значения будут кешироваться в кешах отдельных процесоров, и тут даже атомарность int не поможет.
Начну с того, что (как мне кажется) понимаю, а потом задам вопросы, если позволите .
Lucker пишет:
настаиваю на не путать атомарность и потокобезопасность!
Я не знаю, есть ли классическое определение атомарности (наверняка есть), но я всегда думал о нем так: если операция атомарна, то, какой бы продолжительной она не была, результат операции можно точно предсказать в момент ее начала. Атомарность гарантируется средой исполнения (ОС, СУБД, VM, ect.).
Например, атомарной может быть операция записи массива байт в файл. Она занимает какое-то время, но, используя ее, я могу быть уверен, что во время записи массива другие потоки не смогут писать в тот же файл. Иными словами, никакой другой процесс не сможет повлиять на результат операции. А значит в начале выполнения операции я точно знаю результат (возможность аппаратного сбоя не рассматривается). При этом ОС может отдать процессор другому процесу, прервав мой в середине операции записи (Пример немного не реальный, но от него этого и не требуется ).
Потокобезопасность. Ее я понимаю так: потокобезопасным кодом я называю такой, который для каждого потока даст результат, который бы они получили, работая по одному (например, последовательно). Т.е., независимо от того, сколько потоков исполняют код (или используют общие данные), они не влияют друг на друга.
Теперь по теме наконец (если еще кто-то читает):
— JSL,SE. Ищем упоминание атомарности и потокобезопасности и все, что на это похоже.
JLS, SE:17.1 Terminology and Framework
A variable is any location within a program that may be stored into. <skip> Variables are kept in a main memory that is shared by all threads. <skip> 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. <skip> the verbs use, assign, load, store, lock, and unlock name actions that a thread can perform. The verbs read, write, lock, and unlock name actions that the main memory subsystem can perform. Each of these actions is atomic (indivisible).
Получается, согласно спецификации, есть несколько атомарных операций, которые не являются ни частью языка Java, ни байткодами для JVM (однако, информация о них входит в обе спецификации). Важно, что хоть что-то атомарное есть.
Предположим есть код:
class Sample {
int a = 1, b = 2;
void hither() {
a = b;
}
void yon() {
b = a;
}
}
Поток, выполняя метод hither, должен сделать следующее:
1. read b — memory subsystem, читает значение "b" из общей памяти
2. load b — current thread, записывает значение "b" в свою локальную память
3. use b — current thread, ???
4. assign a — current thread, ???
5. store a — current thread, записывает значение "a" в свою локальную память
6. write a — memory subsystem, записать значение "a" в общую память
Насколько я понимаю, use означает любое использование, кроме присвоения результата. Подобным образом можно описать и инкремент:
class Sample {
int a = 1;
void hither() {
a++;
}
}
Вот так:
1. read a — memory subsystem, читает значение "a" из общей памяти
2. load a — current thread, записывает значение "a" в свою локальную память
3. use a — current thread, здесь это инкремент.
4. assign a — current thread, ???
5. store a — current thread, записывает значение "a" в свою локальную память
6. write a — memory subsystem, записать значение "a" в общую память
Т.е. шесть атомарных метаопераций (назовем их так), но атомарности инкремента нет. Если представить себе поток, который выполнил шаг 3 и был прерван. И второй поток, который выполняет этот же код и тоже дошел до шага 3. Оба скопировали из общей памяти значение переменной "a" равное "1". Дальше не важно, какой поток пройдет все 6 шагов первым. Первый поток сохранит значение "2" в общую память. Второй перезапишет результат первого своим результатом (тоже равным "2"). Этот результат подтверждается тестом, предложенным dshe.
Важно отметить тут, что шаг 6 может не выполняться сразу после шага 5. Ведь результат может быть использован дальше.
class Sample {
int a = 1, b = 7;
void hither() {
a++;
b = b + a;
}
}
Уф... Лирическое отступление:
Больше года назад (когда текущей была JLS, SE) в нескольких форумах и переписке с разными людьми я задавал вопросы, касающиеся последовательности выполнения, оптимизации во время выполнения, etc. Я считал (и считаю) главу 17 JLS SE слишком сложной и непонятно. Иногда даже противоречивой (Меня больше всего интересовали volatile переменные. О них ниже ). Как выяснилось, Sun признала, что их реализация JRE нарушает спецификацию в отношении volatile переменных. (Я где-то видел что-то типа теста, который это подтверждал, но сам я не проверял, так что — за что купил, за то продаю ).
Теперь по потокобезопасности.
class Sample {
int a = 1;
void hither() {
a++;
}
}
Если бы инкремент был атомарной операцией, то код был бы потокобезопасным (в свете всего сказанного выше). Но это не так (тест, предложенный dshe).
Lucker пишет:
A>Я может чего не так понимаю ...
ну это всего лишь значит что при выполнении таких операций в многопоточной среде ты не получишь "неожиданного" результата, как например в случае с long-ами, где старшие байты могут быть изменены в одном потоке, младшие в другом и в результате получится не рыба не мясо.
Поддерживаю Lucker. Я это понимаю так: Речь идет об метооперации "use". Допустим у нас есть код:
class Sample {
int a = 1, b = 7, c = 1;
void hither() {
c = b + a;
}
}
Согласно спецификации, это выглядит так:
1. read a — memory subsystem, читает значение "a" из общей памяти
2. load a — current thread, записывает значение "a" в свою локальную память
3. read b — memory subsystem, читает значение "b" из общей памяти
4. load b — current thread, записывает значение "b" в свою локальную память
5. use a,b — current thread, здесь это инкремент.
6. assign c — current thread, ???
7. store c — current thread, записывает значение "a" в свою локальную память
8. write c — memory subsystem, записать значение "a" в общую память
Вот шаг 5 — это сложение, которое атомарное . Повторюсь, это лишь моя версия событий. Так я понимаю спецификацию.
Первое замечание: здравый смысл говорит мне, что шаги 1 и 2 нельзя менять местами, но шаги 1 и 2 могут быть исполнены после шагов 3 и 4.
1. read b — memory subsystem, читает значение "b" из общей памяти
2. load b — current thread, записывает значение "b" в свою локальную память
3. read a — memory subsystem, читает значение "a" из общей памяти
4. load a — current thread, записывает значение "a" в свою локальную память
Эти две группы метаопераций не влияют друг на друга, не так ли?
Второе замечание: перед use не обязательно будет read и load. А после assign не всегда store и write. Т.е. копия переменной в рабочей области памяти текущего процесса не обязана синхронизироваться с мастер-копией после и перед каждой операции(ей). Как раз это позволяет хранить переменный потока в регистрах, например.
Теперь пара слов о volatile переменных. Что они могут и чего не могут. Выжимка из 17.7 Rules for Volatile Variables:
— Перед каждым использованием (use) значения переменных синхронизируются с мастер-копиями (read, load).
— После каждого использования (assign) значения переменных синхронизируются с мастер-копией (store, write).
— Последовательность синхронизации volatile переменных с мастер-копиями не может быть изменена. (Это относиться к одному потоку)
JLS, SE:
The load, store, read, and write actions on volatile variables are atomic, even if the type of the variable is double or long.
Делает ли модификатор volatile операции с переменной атомарным? Нет. Потокобезопасным? Тоже нет.
korostoff пишет:
Вилимо потому что последовательность
загрузка volatile n1 из master memory, инкремент, сохранение нового значения n1 в master memory
само по себе не атомарно.
В точку. Это тоже очень интересная тема. Многие, с кем я общался по проблеме volatile переменных (в том числе и я по началу) считали, что volatile — это что-то вроде "монитора для примитивов" . Т.е. я предполагал, что объявив переменную с модификатором volatile:
class TestVolatile {
private volatile int foo = 1;
public void up() {
foo++;
}
}
я получу такое поведение:
class TestVolatile {
private int foo = 1;
public synchronized void up() {
foo++;
}
}
Два потока вызывают метод. Тут все просто. Потоки синхронизируются на мониторе экземпляра TestVolatile. Многие считают, что первая версия (с использованием volatile) является способом "синхронизации на мониторе примитива". С примитивами не связаны мониторы и такая версия событий, что называется, reasonable. Но увы, это не так.
Подводя черту под volatile: все, что они могут делать это заменить один тип рассинхронизации при работе нескольких процессов на другой . Какой в этом смысл и к что на самом деле хотели сделать — я не знаю. Вот с этим и живу .
К сожалению, в деталях JMM JLS3 я не разбирался. Я прочитал jsr в надежде, что она станет проще и понятнее . Что могу сказать — она точно изменилась...
А теперь вопрос:
— Все что я писал вполне может оказаться бредом. Пожалуйста, поправьте меня.
— Поразмышляв еще немного над всем этим, можно прийти к выводу, что JLS не запрещает JVM переключать процессы в середине исполнения очередного байткода. Справедливо ли это для JLS3?
— Все эти сложности со спецификацией направлены на то, чтобы дать разработчикам JVM как можно больше свободы при реализации многопороцесности. Это понятно. А вот кто-нибудь может авторитетно заявить, как реализована многопроцесность в JVM для WIN32, например? Т.е. создание потока выполнения создает процесс или поток? Т.е. насколько это дорогая операция? А как с этим в Linux например?
Спасибо всем, кто дочитал . Большое спасибо тем, кто ответит !
Спасибо!
Всетаки атомарность определяется на уровне инструкций байт кода, или же это вообще отдельные вещи?
Т.е. чтоже конкретно можно считать атомарным?
Re[2]: Не делимые операции потоков
От:
Аноним
Дата:
15.06.06 06:58
Оценка:
Здравствуйте, boluba, Вы писали:
B>Подводя черту под volatile: все, что они могут делать это заменить один тип рассинхронизации при работе нескольких процессов на другой . Какой в этом смысл и к что на самом деле хотели сделать — я не знаю. Вот с этим и живу .
Бытует мнение что volatile необходим для того чтобы поток гарантированно "увидел" изменение переменной (из другого потока). То есть якобы в многопроцессорной среде один поток может изменить переменную, а второй поток может бесконечно долго продолжать работать с working copy и "не заметить" ее изменение.
Правда ли это?
Здравствуйте, Аноним, Вы писали:
А>Бытует мнение что volatile необходим для того чтобы поток гарантированно "увидел" изменение переменной (из другого потока). То есть якобы в многопроцессорной среде один поток может изменить переменную, а второй поток может бесконечно долго продолжать работать с working copy и "не заметить" ее изменение. А>Правда ли это?
Да, если переменная не volatile или если между запись в одном потоке/чтение в другом не было memory barrier (т.е. если после записи не освобожден монитор, а перед чтением не получен ТОТЖЕ монитор).
Что особенно интересно в старой memory model (читай глава Threading JLS) при освобожении монитра в master memory флашаться (записываются) ВСЕ иземененные переменные, а при захвате монитора "сбрасывается" (reset) ВСЯ working memory потока. В новой же memory model (JLS 3.0) вообще убраны понятия working/master memory. Там введено понятие "happens before ordering" и в соотвтетсвии с ним JVM иммет право при освобожедении монитора не сбрасывать измененные переменные в master memory если JVM точно уверен что эта измененная переменная не будет читаться другим потоком захвативжим ТОТ ЖЕ монитор.
Т.е. если раньше можно было бы написать так (хотя делать так не советую)
synchronized (new Object) {
} // memory barrierint resA = this.a
int resB = this.b;
То теперь следуя спецификации этот код не верен, т.к. JVM не обязана обеспечивать видимость изменений shared переменных если операции "запись в потоке1"/"чтение в потоке2" не следуют happens-before-ordering (а они не следуют если не между этими операциями нет синхронизации на одном и том же объекте или записи/чтения одной и той же volatile переменной)
Здравствуйте, Tiraspol, Вы писали:
T>в какие операции не могут вклиниваться другие потоки.
T>Вот например i++ — потоко безопасная? T>а i = i +1, или i = Item.foo() + i + i; T>Есть ли понятия минимальной потоковой операции?
T>Спасибо.
Есть интересная главка нумер 17 в Java Language Specification.
Вообще ее всю конечно нехило прочитать, но вот хотя-бы абзац:
17.7 Non-atomic Treatment of double and long
Some implementations may find it convenient to divide a single write action
on a 64-bit long or double value into two write actions on adjacent 32 bit values.
For efficiency's sake, this behavior is implementation specific; Java virtual
machines are free to perform writes to long and double values atomically or in two
parts.
For the purposes of the Java programming language memory model, a single
write to a non-volatile long or double value is treated as two separate writes: one
to each 32-bit half. This can result in a situation where a thread sees the first 32
bits of a 64 bit value from one write, and the second 32 bits from another write.
Writes and reads of volatile long and double values are always atomic. Writes to
and reads of references are always atomic, regardless of whether they are implemented
as 32 or 64 bit values.
VM implementors are encouraged to avoid splitting their 64-bit values where
possible. Programmers are encouraged to declare shared 64-bit values as volatile
or synchronize their programs correctly to avoid possible complications.
Re[3]: Не делимые операции потоков
От:
Аноним
Дата:
15.06.06 13:50
Оценка:
Здравствуйте, aefimov, Вы писали:
A>Спасибо! A>Всетаки атомарность определяется на уровне инструкций байт кода, или же это вообще отдельные вещи? A>Т.е. чтоже конкретно можно считать атомарным?
Я верю в спецификации . Ни в JLS, ни в JVMSpec нет ничего относительно переключения потоков и связанных с этим гарантий. Это сделано намерено, т.к. это дает большую свободу при реализации. Хочешь, переложи все на ОС, нет возможности, делай управление потоками самостоятельно.
Как я уже писал, JLS объявляет атомарными "странные" метаоперации. Они не входят в состав языка Java, но это и не байт коды. Застряли где-то посередине. Это как бы описание действий JVM, которые должны быть гарантированно атомарными. А для этого, очевидно, в каждой JVM подобные метаоперации должны быть выделимы логически. Цель создателей JLS ясна — наложить как можно меньше ограничений на реализацию и дать как можно больше гарантий времени выполнения. О результатах можно спорить.
Мое мнение:
— исполнение одного байткода (любого) нельзя считать атомарным.
— это следовало бы гарантировать, хотя всех последствий я оценить не могу.
CHAPTER 3 INSTRUCTION SET REFERENCE, A-M
. . . INC—Increment by 1
Adds 1 to the destination operand, while preserving the state of the CF flag. The destination
operand can be a register or a memory location. . . .
This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically.
т.е.
lock inc mem
выполняется атомарно, а
inc mem
уже нет.
--
Дмитро
Re[4]: Не делимые операции потоков
От:
Аноним
Дата:
15.06.06 15:28
Оценка:
Здравствуйте, korostoff, Вы писали:
K>Да, если переменная не volatile или если между запись в одном потоке/чтение в другом не было memory barrier (т.е. если после записи не освобожден монитор, а перед чтением не получен ТОТЖЕ монитор).
Тут я вас не понял. Почему именно "тотже" монитор? Поясните пожалуйста
K>Что особенно интересно в старой memory model (читай глава Threading JLS) при освобожении монитра в master memory флашаться (записываются) ВСЕ иземененные переменные, а при захвате монитора "сбрасывается" (reset) ВСЯ working memory потока. В новой же memory model (JLS 3.0) вообще убраны понятия working/master memory. Там введено понятие "happens before ordering" и в соотвтетсвии с ним JVM иммет право при освобожедении монитора не сбрасывать измененные переменные в master memory если JVM точно уверен что эта измененная переменная не будет читаться другим потоком захвативжим ТОТ ЖЕ монитор.
Вот здесь тоже есть разногласия в понимании JLS . Вы пишите "ВСЕ иземененные переменные", а есть мнение, что только те, к которым относиться замок. Иными словами, освобождения замка объекта приводит к записи данных объекта в общую память. А если процесс менял данные других объектов?
K>Т.е. если раньше можно было бы написать так (хотя делать так не советую)
K>поток1: K>
K>То теперь следуя спецификации этот код не верен, т.к. JVM не обязана обеспечивать видимость изменений shared переменных если операции "запись в потоке1"/"чтение в потоке2" не следуют happens-before-ordering (а они не следуют если не между этими операциями нет синхронизации на одном и том же объекте или записи/чтения одной и той же volatile переменной)
Здравствуйте, dshe, Вы писали:
D>Верно. D>В догонку: инструкции процессора могут выполняться неатоматно, что уж говорить о байткодах. D> <skiped>
inc mem
D>уже нет.
Правильно ли я понимаю, что без префикса lock команда будет запущена заново после возврата в этот поток исполнения?
Хотя это скорее для другого форума вопрос.
Здравствуйте, boluba, Вы писали:
B>Здравствуйте, dshe, Вы писали:
D>>Верно. D>>В догонку: инструкции процессора могут выполняться неатоматно, что уж говорить о байткодах. D>> <skiped> B>
B>inc mem
B>
D>>уже нет.
B>Правильно ли я понимаю, что без префикса lock команда будет запущена заново после возврата в этот поток исполнения? B>Хотя это скорее для другого форума вопрос.
Не совсем. Префикс lock не играет существенной роли, если машина однопроцессорная, поскольку аппаратные прерывания не могут прервать выполнение инструкции где-то на середине (хотя, возможно, для векторных инструкций есть нюансы). Однако если машина многопроцессорная, то пока один процессор (выполняя инструкцию inc) вычитав данные выполняет арифметическое сложение, другой процессор может вклиниться и тоже обратиться к памяти (считав еще неизмененные данные или записав значение, которое в последствии будет перезаписано). Префикс lock предотвращает такую ситуацию просто блокируя доступ к общей памяти для других процессоров на время выполнения текущей инструкции.