Использование ключевого слова volatile с изменяемым объектом
В Java я понимаю, что ключевое слово volatile
обеспечивает видимость переменных. Вопрос в том, является ли переменная ссылкой на изменяемый объект, а volatile
также обеспечивает видимость членов внутри этого объекта?
В приведенном ниже примере работает ли он, если несколько потоков обращаются к volatile Mutable m
и меняют value
?
Пример
class Mutable {
private int value;
public int get()
{
return a;
}
public int set(int value)
{
this.value = value;
}
}
class Test {
public volatile Mutable m;
}
Ответы
Ответ 1
Это своего рода пояснение по некоторым деталям волатильности. Написание этого здесь, потому что это слишком много для комментария. Я хочу привести несколько примеров, которые показывают, как изменчивость влияет на видимость и как это изменилось в jdk 1.5.
Учитывая следующий пример кода:
public class MyClass
{
private int _n;
private volatile int _volN;
public void setN(int i) {
_n = i;
}
public void setVolN(int i) {
_volN = i;
}
public int getN() {
return _n;
}
public int getVolN() {
return _volN;
}
public static void main() {
final MyClass mc = new MyClass();
Thread t1 = new Thread() {
public void run() {
mc.setN(5);
mc.setVolN(5);
}
};
Thread t2 = new Thread() {
public void run() {
int volN = mc.getVolN();
int n = mc.getN();
System.out.println("Read: " + volN + ", " + n);
}
};
t1.start();
t2.start();
}
}
Поведение этого тестового кода хорошо определено в jdk1.5 +, но не четко определено pre-jdk1.5.
В мире pre-jdk1.5 не было определённой взаимосвязи между изменчивыми доступами и нелетучими обращениями. поэтому выход этой программы может быть:
- Читайте: 0, 0
- Читать: 0, 5
- Читать: 5, 0
- Читать: 5, 5
В мире jdk1.5 + семантика volatile была изменена, так что волатильные обращения влияют на энергонезависимые обращения точно так же, как синхронизация. поэтому в мире jdk1.5 + возможны только определенные выходы:
- Читайте: 0, 0
- Читать: 0, 5
- Чтение: 5, 0 < - невозможно
- Читать: 5, 5
Выход 3. невозможен, потому что показание "5" из volatile _volN устанавливает точку синхронизации между двумя потоками, что означает, что все действия из t1, принятые до назначения на _volN, должны быть видимыми для t2.
Дальнейшее чтение:
Ответ 2
В вашем примере ключевое слово volatile
гарантирует, что последняя ссылка, написанная любым потоком, на "m" будет видна для любого потока, читающего "m" впоследствии.
Это не гарантирует ничего о вашем get().
Итак, используйте следующую последовательность:
Thread-1: get() returns 2
Thread-2: set(3)
Thread-1: get()
для вас вполне законно возвращаться 2, а не 3. volatile
ничего не меняет.
Но если вы измените свой класс Mutable
на это:
class Mutable {
private volatile int value;
public int get()
{
return a;
}
public int set(int value)
{
this.value = value;
}
}
Тогда гарантируется, что второй get()
из Thread-1 вернет 3.
Обратите внимание, однако, что volatile
обычно не лучший метод синхронизации.
В вашем простом примере get/set (я знаю это просто пример) класс, подобный AtomicInteger
, используя правильную синхронизацию и фактически предоставляя полезные методы, будет лучше.
Ответ 3
volatile
предоставляет только гарантии относительно ссылки на объект, объявленный таким образом. Члены этого экземпляра не синхронизируются.
В соответствии с Wikipedia у вас есть:
- (Во всех версиях Java) Существует глобальный порядок чтения и пишет переменную volatile. Эта подразумевает, что каждый поток, обращающийся к volatile field будет читать текущий перед продолжением, а не (потенциально) с использованием кешированного значения. (Тем не менее, нет никаких гарантий относительно относительный порядок летучих читает и пишет с помощью обычных чтений и пишет, что это обычно не полезная резьба построить.)
- (В Java 5 или более поздней версии) Неустойчивые чтения и записи устанавливают случившееся раньше отношений, так же, как приобретение и освобождение мьютекса.
Итак, в основном у вас есть то, что, объявив поле volatile
, взаимодействие с ним создает "точку синхронизации", после чего любые изменения будут видны в других потоках. Но после этого использование get()
или set()
несовместимо. Java Spec содержит более подробное объяснение.
Ответ 4
volatile
не обеспечивает видимость. Единственным его эффектом является предотвращение кэширования процессора переменной, что обеспечивает связь между событиями и одновременными чтениями и записью. Он не влияет на элементы объекта и не обеспечивает блокировку synchronization synchronized
.
Как вы не сказали нам, что такое "правильное" поведение вашего кода, на вопрос нельзя ответить.
Ответ 5
Использование значения volatile
, а не полностью synchronized
, по существу, является оптимизацией. Оптимизация исходит из более слабых гарантий, обеспечиваемых значением volatile
по сравнению с доступом synchronized
. Преждевременная оптимизация - корень всего зла; в этом случае зло может быть трудно отследить, потому что оно будет в форме условий гонки и тому подобное. Поэтому, если вам нужно спросить, вы, вероятно, не должны его использовать.