Есть ли смысл использовать летучую долготу? - программирование
Подтвердить что ты не робот

Есть ли смысл использовать летучую долготу?

Я иногда использую переменную экземпляра 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 свойства в основной поток.

4b9b3361

Ответ 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) {}.

Есть ли еще?