Есть ли смысл использовать летучую долготу?
Я иногда использую переменную экземпляра volatile
в случаях, когда у меня есть два потока, которые читают/записывают в него и не хотят, чтобы накладные расходы (или потенциальный риск взаимоблокировки) вынимали блокировку; например, поток таймера, периодически обновляющий идентификатор int, который отображается как получатель в каком-либо классе:
public class MyClass {
private volatile int id;
public MyClass() {
ScheduledExecutorService execService = Executors.newScheduledThreadPool(1);
execService.scheduleAtFixedRate(new Runnable() {
public void run() {
++id;
}
}, 0L, 30L, TimeUnit.SECONDS);
}
public int getId() {
return id;
}
}
Мой вопрос: Учитывая, что JLS только гарантирует, что 32-разрядные чтения будут атомарными, есть ли какая-нибудь точка в когда-либо, используя долговременную летучую? (то есть 64-бит).
Предостережение. Пожалуйста, не отвечайте, говоря, что использование volatile
over synchronized
- это пример предварительной оптимизации; Мне хорошо известно, как и когда использовать synchronized
, но бывают случаи, когда volatile
является предпочтительным. Например, при определении Spring bean для использования в однопоточном приложении я склонен использовать переменные экземпляра volatile
, так как нет гарантии, что контекст Spring инициализирует каждый bean свойства в основной поток.
Ответы
Ответ 1
Не уверен, правильно ли я понял ваш вопрос, но JLS 8.3.1.4. volatile Fields:
Поле может быть объявлено изменчивым, и в этом случае модель памяти Java гарантирует, что все потоки будут видеть согласованное значение для переменной (§17.4).
и, что более важно, JLS 17.7 Неатомная обработка двойного и длинного:
17.7 Неатомная обработка двойного и длинного
[...]
Для целей модели памяти языка программирования Java одна запись в энергонезависимое длинное или двойное значение рассматривается как две отдельные записи: одна на каждую 32-битную половину. Это может привести к ситуации, когда поток видит первые 32 бита 64-битного значения из одной записи, а второй 32 бита из другой записи. Записывает и читает изменчивые длинные и двойные значения, всегда атомарные. Записывает и читает ссылки всегда атомарно, независимо от того, реализованы ли они как 32 или 64-битные значения.
То есть, "целая" переменная защищена изменчивым модификатором, а не только двумя частями. Это искушает меня утверждать, что еще более важно использовать volatile для long
, чем для int
, так как даже чтение не является атомарным для долговременных длин/удвоений.
Ответ 2
Это можно продемонстрировать на примере
- постоянно переключать два поля, один помеченный volatile и один не между всеми установленными битами, а все биты очищаются
- читать значения полей в другом потоке
- см., что поле foo (не защищенное volatile) может быть прочитано в несогласованном состоянии, это никогда не происходит с защищенным полем с изменчивым
код
public class VolatileTest {
private long foo;
private volatile long bar;
private static final long A = 0xffffffffffffffffl;
private static final long B = 0;
private int clock;
public VolatileTest() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
foo = clock % 2 == 0 ? A : B;
bar = clock % 2 == 0 ? A : B;
clock++;
}
}
}).start();
while (true) {
long fooRead = foo;
if (fooRead != A && fooRead != B) {
System.err.println("foo incomplete write " + Long.toHexString(fooRead));
}
long barRead = bar;
if (barRead != A && barRead != B) {
System.err.println("bar incomplete write " + Long.toHexString(barRead));
}
}
}
public static void main(String[] args) {
new VolatileTest();
}
}
Выход
foo incomplete write ffffffff00000000
foo incomplete write ffffffff00000000
foo incomplete write ffffffff
foo incomplete write ffffffff00000000
Обратите внимание, что это происходит только для меня при запуске на 32-битной виртуальной машине, на 64-битной виртуальной машине я не мог получить одну ошибку за несколько минут.
Ответ 3
"volatile" выполняет несколько задач:
- гарантирует, что атомная запись будет двойной/длинной
- гарантирует, что, когда поток A видит изменение изменчивой переменной, сделанной потоком B, поток A также может видеть все другие изменения, сделанные потоком B, перед изменением на изменчивую переменную (подумайте, устанавливая количество используемых ячеек в массиве после установки сами клетки).
- предотвращает оптимизацию компилятора на основе предположения, что только один поток может изменить переменную (подумайте об узкой петле
while (l != 0) {}
.
Есть ли еще?