Имеется единственный экземпляр класса, к которому в течение часа могут обращаться разные потоки для чтения и модификации. Операции синхронизированы, тут проблем нет. В экземпляре класса много всякого и разного.
Раз в час этот экземпляр должен заменяться на новый экземпляр, который в течение прошедшего часа изготавливался.
По логике вещей эту замену надо проводить атомарно. Но тогда все методы чтения и изменения в течение часа должны добираться к этому экземпляру через блокировку. Пустая трата времени в течение часа.
Между тем, если некий поток все же получит еще раз старое значение этой переменной, ничего плохого не будет по условиям задачи. Просто однажды она должна быть обновлена, а до тех пор можно и старое значение использовать. Минутой ранбше или позже — не важно.
Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым.
А вот в Яве боюсь. Что, если в это время влезет GC и посчитает нужным удалить старый экземпляр до того, как ссылка обновилась ? Или это невозможно ?
Хотелось бы знать мнение по этому вопросу, как лучше сделать.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Имеется единственный экземпляр класса, к которому в течение часа могут обращаться разные потоки для чтения и модификации. Операции синхронизированы, тут проблем нет. В экземпляре класса много всякого и разного.
PD>Раз в час этот экземпляр должен заменяться на новый экземпляр, который в течение прошедшего часа изготавливался.
PD>По логике вещей эту замену надо проводить атомарно. Но тогда все методы чтения и изменения в течение часа должны добираться к этому экземпляру через блокировку. Пустая трата времени в течение часа.
Если я правильно понял, то операции определенные в данном классе уже синхронизированы, т.е. не требует какой-либо дополнительной блокировки со стороны вызывающего кода. В таком случае разве недостаточно сделать переменную, которая хранит ссылку на текущий экземпляр volatile? В таком случае блокировки для получения данной ссылки не требуется.
PD>А вот в Яве боюсь. Что, если в это время влезет GC и посчитает нужным удалить старый экземпляр до того, как ссылка обновилась ? Или это невозможно ?
Никаких проблем тут с GC быть не должно. В любой момент времени на экземпляр класса ссылается либо статическая volatile переменная через которую организован доступ к объекту, либо локальный фрейм одного из потоков который работает с объектом. Пока эти ссылки есть (а они будут существовать до тех пор пока 1) вы не обновите статическую переменную; 2) все потоки не закончят работать со старым объектом), GC ничего удалять не имеет права.
It is always bad to give advices, but you will be never forgiven for a good one.
Oscar Wilde
Здравствуйте, Pavel Dvorkin, Вы писали:
PD> Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым.
Можно, без проблем. Просто объявить volatile.
Если нужны более хитрые операции (CAS например), можно посмотреть AtomicReference.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым.
Да можно, причем writes and reads are atomic, насколько я помню
PD>А вот в Яве боюсь. Что, если в это время влезет GC и посчитает нужным удалить старый экземпляр до того, как ссылка обновилась ? Или это невозможно ?
невозможно такое поведение. к вашему объекту всегда будет путь от рута какого нибудь треда и его никто не соберет.
PD>Хотелось бы знать мнение по этому вопросу, как лучше сделать.
нужно не забыть про volatile наверное, или использовать AtomicReference, но он здесь оверкил
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым.
Хорошо звучит — "я бы рискнул" без барьеров, не факт, что другие потоки смогут это изменение увидеть.
PD>А вот в Яве боюсь. Что, если в это время влезет GC и посчитает нужным удалить старый экземпляр до того, как ссылка обновилась ? Или это невозможно ?
невозможно, как уже говорили, если есть ссылка из любого потока, то объект никуда не денется.
PD>Хотелось бы знать мнение по этому вопросу, как лучше сделать.
запись в volatile гарантирует, что сработает барьер и все предыдущие изменения будут видны другим потокам. Так же volatile гарантирует happens-before, не знаю насколько это актуально для данного случая.
апдейт ссылок по-любому атомарен.
Здравствуйте, ekamaloff, Вы писали:
E>Если я правильно понял, то операции определенные в данном классе уже синхронизированы, т.е. не требует какой-либо дополнительной блокировки со стороны вызывающего кода. В таком случае разве недостаточно сделать переменную, которая хранит ссылку на текущий экземпляр volatile? В таком случае блокировки для получения данной ссылки не требуется.
Вот это я не совсем понимаю. volatile гарантирует, что изменения будут видны другим потокам, то есть что другие потоки не сохранят у себя копии, а должны будут читать из оригинала каждый раз. Это замечательно, но какое отношение это имеет к блокировке ? Чтение переменнной volatile обязательно идет в одну команду ?
Поток A начинает читать переменную. Считывает ее на регистр X.
Поток B изменяет переменную
Поток A заканчивает чтение , засылая из регистра X в регистр Y.
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Здравствуйте, ekamaloff, Вы писали:
E>>Если я правильно понял, то операции определенные в данном классе уже синхронизированы, т.е. не требует какой-либо дополнительной блокировки со стороны вызывающего кода. В таком случае разве недостаточно сделать переменную, которая хранит ссылку на текущий экземпляр volatile? В таком случае блокировки для получения данной ссылки не требуется.
PD>Вот это я не совсем понимаю. volatile гарантирует, что изменения будут видны другим потокам, то есть что другие потоки не сохранят у себя копии, а должны будут читать из оригинала каждый раз. Это замечательно, но какое отношение это имеет к блокировке ? Чтение переменнной volatile обязательно идет в одну команду ?
Грубо говоря volatile гарантирует что обновленное значение переменной будет немедленно видно другим потокам. Упрощенное объяснение и пример можно найти здесь, а более формальное определение семантики volatile в контексте "порядка синхронизации" в секции 17.4.4 здесь (англ.):
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).
Запись volatile переменной v синхронизирована-с последующими чтениями v любым потоком (где "последующий" определено в контексте "порядка синхронизации")
Кроме того, независимо от того, является ли переменная volatile или нет, запись и чтение переменной ссылочного типа в Java — атомарная операция (это справедливо и для примитивных типов). Как бы ни конкурировали с собой потоки в вашем приложении, у вас всегда есть гарантия того, что переменная содержит либо null или валидную ссылку на объект (без volatile эта переменная может быть устаревшей локальной копией и может вести к непредсказуемым последствиям относительно видимого порядка выполнения некоторых операций — см. пример по первой ссылке выше).
Блокировка при чтении и записи (используя synchronized или java.util.concurrent.lock.*) — это в вашей ситуации альтернатива volatile, которая имеет более сложную семантику: Обеспечивает немедленное обновления значения переменной во всех потоках (подобно volatile)
Обеспечивает семантику happens-before (так же как и volatile, по-моему начиная с версии 5)
Обеспечивает взаимо-исключающее (мьютекс) выполнение данного участка кода разными потоками
В вашей задаче реально требуется только (1), (2) по большому счету не нужно и в любом случае обеспечивается volatile, (3) — лишнее и может значительно ухудшить производительность кода.
PD>Поток A начинает читать переменную. Считывает ее на регистр X. PD>Поток B изменяет переменную PD>Поток A заканчивает чтение , засылая из регистра X в регистр Y.
Как я сказал выше — чтение и запись ссылочной переменной атомарны (это не связано с volatile). Как только поток A считал переменную в локальный стек, его больше не должно волновать что произойдет с исходной volatile переменной.
It is always bad to give advices, but you will be never forgiven for a good one.
Oscar Wilde
Здравствуйте, ekamaloff, Вы писали:
E>Как я сказал выше — чтение и запись ссылочной переменной атомарны (это не связано с volatile). Как только поток A считал переменную в локальный стек, его больше не должно волновать что произойдет с исходной volatile переменной.
Здравствуйте, ekamaloff, Вы писали:
E>Кроме того, независимо от того, является ли переменная volatile или нет, запись и чтение переменной ссылочного типа в Java — атомарная операция (это справедливо и для примитивных типов).
Запись non-volatile long и dobule все же может быть неатомарной. Non-atomic Treatment of double and long. Ссылки и все остальные примитивные типы — да, атомарны всегда.
Здравствуйте, StanislavK, Вы писали:
PD>>Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым. SK>Хорошо звучит — "я бы рискнул" без барьеров, не факт, что другие потоки смогут это изменение увидеть.
Гм. Что-то я не понял. Один поток меняет значение переменной. Пусть даже неатомарно. Другой поток читает. Мне что, все такие переменные объявлять volatile ? А массивы ? Иные структуры данных ? Так ведь можно договориться до того, что все изменения в памяти надо проводить атомарно, иначе их другие потоки не увидят
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>>>Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым. SK>>Хорошо звучит — "я бы рискнул" без барьеров, не факт, что другие потоки смогут это изменение увидеть.
PD>Гм. Что-то я не понял. Один поток меняет значение переменной. Пусть даже неатомарно. Другой поток читает. Мне что, все такие переменные объявлять volatile ? А массивы ? Иные структуры данных ? Так ведь можно договориться до того, что все изменения в памяти надо проводить атомарно, иначе их другие потоки не увидят
Все примитивные значения (т.е. примитивные типы + ссылка) к которым возможен доступ из разных потоков очень желательно объявлять volatile. Скажем, такой код:
class MyObj
{
int value;
MyObj(int value){this.value=value;}
}
...
MyObj shared=new MyObj(1);
...
shared = new MyObj(2); /// thread 1
...
System.out.println(shared.value);/// thread 2
может вывести значение 0, т.к. значение в shared может быть присвоено до того, как 2 присвоится в value.
Если ты объявишь shared как volatile, то будет гарантия happens-before.
Ещё 64-битные значения (т.е. long и double), не помеченные как volatile могут быть испорчены, т.к. присвоение может быть неатомарным (как минимум по спеке, но может быть не на практике).
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Здравствуйте, StanislavK, Вы писали:
PD>>>Вопрос — можно ли рискнуть заменить ее без лока ? На С++ я бы рискнул. Указатель либо изменился, либо нет, но не может же он наполовину измениться. Так что он всегда валиден, только вот может быть либо старым, либо новым. SK>>Хорошо звучит — "я бы рискнул" без барьеров, не факт, что другие потоки смогут это изменение увидеть.
PD>Гм. Что-то я не понял. Один поток меняет значение переменной. Пусть даже неатомарно. Другой поток читает. Мне что, все такие переменные объявлять volatile ? А массивы ? Иные структуры данных ? Так ведь можно договориться до того, что все изменения в памяти надо проводить атомарно, иначе их другие потоки не увидят
Че-то вы там плаваете. Атомарность тут не при чем, да и барьеры применяется не к переменным в отдельности.
volatile гарантирует 3 вещи:
1. Happens-before. Попросту говоря гарантия того, что то, что в коде написано до чтения/записи volatile действительно случится до этого момента. Эта темя хорошо раскрыта в проблеме синглетона в java до 1.5. Вот тут есть пара ссылок на эту тему: http://rsdn.ru/forum/java/4601604.1.aspx
Действует как на уровне компилятора (он не делает оптимизаций, которые меняют порядок) и на уровне процессора. 2. memory visibility. Гарантия того, что изменение значения переменной увидят другие потоки. В JVM spec говорится, что у каждого потока есть своя working memory и что есть main memory, что поток может обновить значение переменной в working memory и оно, в принципе, вообще может не попасть в общую память, видимую всем другим потокам. На практике это всего-лишь метафора для всевозможных оптимизаций, кешей процессора, о том, что в процессоре операции могут происходить (или результаты могут быть видны) не в том порядке, как кажется(это, кстати, тоже, и к happens-before относится). В общем, для всего, что может помешать потокам видеть одни и те же данные и в ожидаемом порядке. Кеши и оптимизации процессора лечатся барьерами, эта тема раскрыта вот тут: http://en.wikipedia.org/wiki/Memory_barrier. Т.к. используются барьеры, то на практике (но не в теории, memory model этого не гарантирует) запись в volatile переменную означает, что все, что было изменено до этой записи будет видно другим потокам. 3. atomicity для примитивных типов и ссылок. Надо заметить, что проблема может возникнуть _только_ для 64-х битных типов на 32-х битных процессорах, что сейчас большая редкость. Раскрывать тут особо нечего.
Так, что вот.. многопоточность, она на самом деле много хуже, чем кажется и читайте Java Memory Model и JVM spec про потоки, там еще много чего написано.
SK>volatile гарантирует 3 вещи:
SK>3. atomicity для примитивных типов и ссылок. Надо заметить, что проблема может возникнуть _только_ для 64-х битных типов на 32-х битных процессорах, что сейчас большая редкость. Раскрывать тут особо нечего.
Не так. Атомарность записи/чтения ссылочных и примитивных типов (за исключением long и double), как уже упоминалось выше, гарантирует язык как таковой, а не volatile. Смотреть здесь или здесь.
volatile лишь добавляет семантику happens-before и memory-consistency (как следствие happens-before) — т.е. любой поток видит 1) последнее записанное значение переменной 2) а также эффект от всех действий совершенных перед записью этой переменной.
Кроме того, volatile расширяет атомарность записи и чтения на long и double.
It is always bad to give advices, but you will be never forgiven for a good one.
Oscar Wilde
SK>Так, что вот.. многопоточность, она на самом деле много хуже, чем кажется и читайте Java Memory Model и JVM spec про потоки, там еще много чего написано.
Да, наверное ты прав. Я с потоками имел дело не раз на С++. Там нет необходимости учитывать этот memory barrier, так как примитивы синхронизации Win API обеспечивают запись измененных данных в RAM. Но там нет никакой working memory у потока (хотя кэш процессора, конечно, есть). Если в Яве ввели еще некую локальную память для потока, то понятно, что необходимо обеспечить ее синхронизацию с основной памятью.
Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ?
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Да, наверное ты прав. Я с потоками имел дело не раз на С++. Там нет необходимости учитывать этот memory barrier, так как примитивы синхронизации Win API обеспечивают запись измененных данных в RAM. Но там нет никакой working memory у потока (хотя кэш процессора, конечно, есть). Если в Яве ввели еще некую локальную память для потока, то понятно, что необходимо обеспечить ее синхронизацию с основной памятью.
Если в java ты используешь synchronized или java.util.concurrent.locks.*, это примерно эквивалентно примитивам синхронизации WinAPI (если ошибаюсь, поправьте!).
Вопрос в том, чтобы избежать явной синхронизации, которая вызывает проблемы с масшабируемостью на кучу ядер. Иногда можно избежать лишней синхронизации, но для явы только начиная с 1.5.
А в C++ working memory тоже есть, там всё гораздо хуже. Скажем, Double Check locking реализовать было(?) нельзя, только в каких-то версиях компиляторов таки можно.
PD>Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ?
Если переносишь код с WinAPI, то не парься c volatile, используй ReadWriteLock — всё будет хорошо.
Для подробностей лучше покажи примерный код, будет проще подумать как же лучше сделать.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, Pavel Dvorkin, Вы писали:
PD>Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ?
Нужно защитить доступ к массиву с помощью synchronized.
Т.е. как-то так (пусть вас не напрягает копирование массивов, оно здесь для простоты примера)
private final byte[] array = new byte[100000];
public void write(int offset, byte[] data) {
synchronized(array) {
System.copyarray(data, 0, array, offset, data.length);
}
}
public void read(int offset, byte[] buffer) {
synchronized(array) {
System.copyarray(buffer, 0, array, offset, buffer.length)
}
}
Синхронизировать на самом массиве конечно необязательно — хотя и вполне логично в данном случае. Главное чтобы синхронизация была на одном и том же мониторе (объекте), который неизменен на протяжении синхронизированного доступа
It is always bad to give advices, but you will be never forgiven for a good one.
Oscar Wilde
Здравствуйте, ., Вы писали:
.>А в C++ working memory тоже есть, там всё гораздо хуже. Скажем, Double Check locking реализовать было(?) нельзя, только в каких-то версиях компиляторов таки можно.
Вообще-то в С++ , по крайней мере до C++0x, ничего об это вообще нет, так как и понятия потока нет. __beginthreadex к стандарту не относится. Синхронизация делается средствами Win API для Windows (С линукс я не работаю). И проблем с этим вроде как не было.
PD>>Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ? .>Если переносишь код с WinAPI, то не парься c volatile, используй ReadWriteLock — всё будет хорошо.
Нет, это не ответ. Будет ли нет ?
.>Для подробностей лучше покажи примерный код, будет проще подумать как же лучше сделать.
Да бога ради, вот пример (псевдокод, конечно)
class A
{
int[] a = new int[1000];
int s;
void f() // вызывается из mainThread
{
for (int i = 0; i < 1000; i++)
a[i] = i;
Thread2.start();
}
int sum() // вызывается из Thread2
{
for (int i = 0; i < 1000; i++)
s+=i;
}
.>>А в C++ working memory тоже есть, там всё гораздо хуже. Скажем, Double Check locking реализовать было(?) нельзя, только в каких-то версиях компиляторов таки можно. PD>Вообще-то в С++ , по крайней мере до C++0x, ничего об это вообще нет, так как и понятия потока нет. __beginthreadex к стандарту не относится. Синхронизация делается средствами Win API для Windows (С линукс я не работаю). И проблем с этим вроде как не было.
Вроде я слышал, что в какой-то стандарт С++ решили внести memory model как в java. Подробностей не знаю.
Проблемы были, если не использовались явные функции синхронизации. Аналогично в java 1.4.
PD>>>Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ? .>>Если переносишь код с WinAPI, то не парься c volatile, используй ReadWriteLock — всё будет хорошо. PD>Нет, это не ответ. Будет ли нет ?
Будет. Даже лучше, чем с простыми локами в C++, т.к. read-операции не будут друг-друга блокировать.
.>>Для подробностей лучше покажи примерный код, будет проще подумать как же лучше сделать. PD>Да бога ради, вот пример (псевдокод, конечно)
PD>Будет сумма корректной или нет ?
Да тут вообще многопоточного доступа нет. Тред запускается только после того, как всё запишется. Интуитивно думаю, что запуск треда гарантирует happens before, хотя ссылку на спеку не дам, искать лень.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, E.K., Вы писали:
EK>Здравствуйте, Pavel Dvorkin, Вы писали:
PD>>Для уточнения — такой вопрос. Пусть я в одном потоке записал элементы некоего 100 Кб массива. Что я должен сделать, чтобы другой поток гарантированно увидел эти данные ?
EK>Нужно защитить доступ к массиву с помощью synchronized.
<skipped>
EK>Синхронизировать на самом массиве конечно необязательно — хотя и вполне логично в данном случае. Главное чтобы синхронизация была на одном и том же мониторе (объекте), который неизменен на протяжении синхронизированного доступа
Что-то здесь не то.
Я понимаю, что во время работы write не сможет read прочитать данные. Это хорошо, но вопрос-то не в этом.
Пусть 100% гарантированно read выполняется после write. synchronized тогда смысла не имеет — в этот монитор никто войти и не попытается, пока он занят. Но мне же StanislavK доказывал, что у потока есть своя memory (ну ладно, деталей этого я не знаю, а вот что уж точно знаю, это то, что у ядра есть кэш.). Предположим, что поток write записал в элементы массива с нулевого по 999 (это ему кажется, что он в RAM записал, а попало только в кеш или эту Java working memory). Поток read читает через пару минут и ? Что он прочитает ? Или сам факт входа в synchronized работает как flush all thread memory ?
Здравствуйте, ., Вы писали:
.>Вроде я слышал, что в какой-то стандарт С++ решили внести memory model как в java. Подробностей не знаю.
Я тоже не в курсе. Пропустим.
PD>>Нет, это не ответ. Будет ли нет ? .>Будет. Даже лучше, чем с простыми локами в C++, т.к. read-операции не будут друг-друга блокировать.
.>>>Для подробностей лучше покажи примерный код, будет проще подумать как же лучше сделать. PD>>Да бога ради, вот пример (псевдокод, конечно)
PD>>Будет сумма корректной или нет ? .>Да тут вообще многопоточного доступа нет. Тред запускается только после того, как всё запишется. Интуитивно думаю, что запуск треда гарантирует happens before, хотя ссылку на спеку не дам, искать лень.
Хорошо, тогда запустим тред заранее
class A
{
int[] a = new int[1000];
int s;
void main()
{
Thread2.start();
f();
Thread2.join();
}
void f() // вызывается из mainThread
{
for (int i = 0; i < 1000; i++)
a[i] = i;
}
int sum() // вызывается из Thread2
{
Thread.sleep(10000);
for (int i = 0; i < 1000; i++)
s+=i;
}