Java: Является ли volatile/final необходимым для ссылки на синхронизированный объект?

Это кажется довольно простой проблемой, но я не могу найти четкое подтверждение.

Скажем, у меня есть класс, правильно синхронизированный сам по себе:

public class SyncClass {

   private int field;

   public synchronized void doSomething() {
       field = field * 2;
   }

   public synchronized void doSomethingElse() {
       field = field * 3;
   }
}

Если мне нужно иметь ссылку для экземпляра этого класса, разделяемого между потоками, , мне все равно нужно объявить этот экземпляр volatile или final, я прав? Как в:

public class MainClass { // previously OuterClass

    public static void main(String [ ] args) {

        final SyncClass mySharedObject = new SyncClass();

        new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomething();
            }
       }).start();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomethingElse();
            }
       }).start();
    }
}        

Или, если mySharedObject не может быть окончательным, поскольку его инстанцирование зависит от некоторых других условий (взаимодействие с GUI, информация из сокета и т.д.), неизвестно заранее:

public class MainClass { // previously OuterClass

    public static void main(String [ ] args) {

        volatile SyncClass mySharedObject;

        Thread initThread = new Thread(new Runnable() {
            public void run() {

            // just to represent that there are cases in which
            //   mySharedObject cannot be final
            // [...]
            // interaction with GUI, info from socket, etc.
            // on which instantation of mySharedObject depends

            if(whateverInfo)
                mySharedObject = new SyncClass();
            else
               mySharedObject = new SyncClass() {
                   public void someOtherThing() {
                     // ...
                   }
               }
            }
       });

       initThread.start();

       // This guarantees mySharedObject has been instantied in the
       //  past, but that still happened in ANOTHER thread
       initThread.join();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomething();
            }
       }).start();

       new Thread(new Runnable() {
            public void run() {
                mySharedObject.doSomethingElse();
            }
       }).start();
    }
}        

Финальные или летучие являются обязательными, тот факт, что MyClass синхронизирует доступ к своим собственным членам, НЕ освобождается от необходимости следить за тем, чтобы ссылка была разделена между потоками. Правильно ли это?

Различия с Разница между изменчивой и синхронизированной в Java

1- Рассматриваемый вопрос касается синхронизации и волатильности в качестве альтернатив для одного и того же поля/переменной, мой вопрос заключается в том, как правильно использовать уже правильно синхронизированный класс (т.е. синхронизирован был выбран), учитывая последствия, которые необходимо учитывать вызывающим, возможно, используя volatile/final по ссылке уже синхронизированного класса.

2- Другими словами, упомянутый вопрос/ответы касаются блокировки/изменчивости. ОДИНОЧНЫЙ ОБЪЕКТ, мой вопрос: как я могу быть уверенным, что разные потоки действительно видят тот же объект? ПЕРЕД фиксацией/доступом к нему.

Когда первый ответ упомянутого вопроса явно ссылается на волатильную ссылку, он касается неизменяемого объекта без синхронизации. Второй ответ ограничивает себя примитивными типами. Я нашел их полезными (см. Ниже), но недостаточно полно, чтобы пролить любые сомнения на то, что я здесь даю.

3. Ответные ответы являются очень абстрактными и научными объяснениями по очень открытым вопросам, совершенно не содержащим никакого кода; как я заявил во вступлении, мне нужно четкое подтверждение фактического кода, ссылающегося на конкретную, хотя и довольно распространенную проблему. Они связаны, конечно, но так же, как учебник относится к конкретной проблеме. (Я действительно прочитал его, прежде чем открывать этот вопрос, и считаю его полезным, но мне все еще нужно обсудить конкретное приложение.) Если в текстовых книгах разрешены все проблемы/сомнения, которые люди могут применять, возможно, нам вообще не понадобится stackoverflow.

Учтите, что при многопоточности вы не можете "просто попробовать", вам нужно правильное понимание и быть уверенным в деталях, потому что условия гонки могут идти прямо тысячу раз, а затем идут ужасно неправильно тысячи + 1 раз.

Ответ 1

Да, вы правы. Необходимо, чтобы вы сделали доступ к переменной также потокобезопасной. Вы можете сделать это, сделав это final или volatile, или убедитесь, что все потоки снова обращаются к этой переменной внутри синхронного блока. Если бы вы этого не сделали, может быть, что один поток "видит" уже новое значение переменной, но, например, другой поток может "видеть" null.

Итак, в отношении вашего примера вы иногда можете получить NullPointerException, когда поток обращается к переменной mySharedObject. Но это может произойти только на многоядерных машинах с несколькими кешами.

Модель памяти Java

Главное здесь - модель памяти Java. Он утверждает, что в потоке гарантируется только обновление памяти другого потока, если это обновление происходит до чтения этого состояния в так называемом деле-зависимом отношении. Связь между событиями и предыдущими может быть реализована с помощью final, volatile или synchronized. Если вы не используете ни одну из этих конструкций, присваивание переменной одним потоком никогда не будет гарантировано видимым каким-либо другим потоком.

Вы можете думать о потоках, чтобы концептуально иметь локальные кеши и до тех пор, пока вы не применяете синхронизацию кешей из нескольких потоков, поток просто читает и записывает в свой локальный кеш. Это может привести к ситуации, когда два потока видят совершенно разные значения при чтении из того же поля.

Обратите внимание, что есть некоторые дополнительные способы обеспечения видимости изменений в памяти, например, с помощью статических инициализаторов. Кроме того, вновь созданный поток всегда видит текущую память его родительского потока без дальнейшей синхронизации. Таким образом, ваш пример может работать даже без синхронизации, потому что создание ваших потоков каким-то образом выполняется после того, как поле было инициализировано. Однако полагаться на такой тонкий факт очень рискованно и может легко ломаться, если вы позже реорганизуете свой код, не имея в виду эту деталь. Более подробная информация о соотношении "случится раньше" описана (но трудно понять) в Спецификация Java Language.

Ответ 2

Если мне нужно иметь подтверждение для экземпляра этого класса, разделяемого между потоками, мне все равно нужно объявить, что этот элемент изменчив или окончательный, я прав?

Да, вы правы. В этом случае у вас есть две общие переменные:

private int field

private SyncClass mySharedObject

Из-за того, как вы определили SyncClass, любая ссылка на SyncClass даст вам самое последнее значение этого SyncClass.

Если вы не синхронизируете доступ к mySharedObject правильно (не конечное, энергонезависимое) поле и вы изменяете значение mySharedObject, вы можете получить mySharedObject, который устарел.

Ответ 3

Это полностью зависит от контекста совместного использования этой переменной.

Вот простой пример, где это хорошо:

class SimpleExample {
    private String myData;

    public void doSomething() {
        myData = "7";

        new Thread(() -> {
            // REQUIRED to print "7"
            // because Thread#start
            // mandates happens-before ordering.
            System.out.println(myData);
        }).start();
    }
}

Ваши приведенные примеры могут подпадать под этот случай. 17.4.5:

  • Если x и y - действия одного и того же потока, а x - до y в порядке программы, тогда hb (x, y).

  • Вызов start() в потоке происходит - перед любыми действиями в запущенном потоке.

Другими словами, если присвоение mySharedObject происходит в том же потоке, который запускает новый поток, новому потоку поручено видеть назначение независимо от синхронизации.

Однако, если вы ожидаете, например, что init может быть вызван в потоке, отличном от того, который вызывает doSomething, тогда у вас может быть состояние гонки.

public static void main(String[] args) {
    final OuterClass myOuter = new OuterClass();

    Thread t1 = new Thread( () -> myOuter.init(true)    );
    Thread t2 = new Thread( () -> myOuter.doSomething() );

    t1.start(); // Does t1#run happen before t2#run? No guarantee.
    t2.start(); // t2#run could throw NullPointerException.
}

Тот факт, что SyncClass имеет синхронизированные методы, совершенно не имеет отношения к гарантированному состоянию ссылки mySharedObject. Чтение этой ссылки выполняется за пределами синхронизированного блока.

В случае сомнений используйте final или volatile. В зависимости от того, что подходит.

Ответ 4

Две вещи, которые следует иметь в виду здесь для понимания:

  • Гонки для вашей ссылочной переменной не имеют принципиального отличия от поля участника.
  • Обмен ссылочной переменной требует тщательной обработки безопасной публикации

Ответ 5

Не обязательно использовать какие-либо из них, но вы должны знать о них, если хотите написать правильный многопоточный код.

Финал

final означает, что вы не можете повторно инициализировать эту переменную снова, поэтому, когда вы скажете

final SyncClass mySharedObject = new SyncClass();

вы не можете выполнить инициализацию mySharedObject снова в другой части кода, как показано ниже

   mySharedObject = new SyncClass(); // throws compiler error

Даже если вы не можете повторно назначить ссылку mySharedObject на какой-либо другой объект, вы можете обновить его состояние (переменную счетчика полей), вызывая методы на нем, потому что field не является окончательным.

Синхронизация и энергозависимость - это просто конструкции, гарантирующие, что любое изменение совместно используемого изменяемого объекта (в данном случае обновление счетчика field) одним потоком будет видимым для всех других потоков.

Синхронизация

synchronized означает, что любой поток, пытающийся вызвать этот метод, должен получить блокировку объекта, в котором этот метод определен.

Итак, в вашем случае, если thread-1 пытается сделать mySharedObject.doSomething(), он будет блокировать на mySharedObject, а thread-2 должен ждать, пока потоки-1 не выпустят блокировки на том же объекте, чтобы они могли do mySharedObject.doSomethingElse() т.е. используя Синхронизацию в любой данный момент времени, только один поток будет обновлять состояние объекта. В конце метода, перед тем как освободить блокировку, все изменения, сделанные потоком-1, будут сброшены в основную память, чтобы поток-2 мог работать в последнем состоянии.

Летучие

volatile, с другой стороны, обеспечивает прозрачность чтения/записи для всех потоков. Любая запись и запись в изменчивую переменную всегда сбрасываются в основную память.

Если переменная field внутри SyncClass является энергозависимой, любое обновление, подобное field++ потоком-1, видимо для потока-2, но я не уверен, как оно относится к объектным ссылкам.

Поскольку volatile только гарантирует видимость, но не атомарность, возможно, что оба потока-1 и thread-2 пытаются обновить счетчик field в то же время, и окончательное обновленное значение может быть неправильным.

Ответ 6

Заключительные или неустойчивые являются обязательными, тот факт, что MyClass синхронизирует доступ к своим собственным членам, НЕ освобождает от необходимости следить за тем, чтобы ссылка была разделена между потоками. Правильно ли это?

Нет, это не мандат из контекста многопоточности, он заставляет вас сделать его окончательным только из-за annonymus inner-class (вы использовали для создания потока)

new Thread(new Runnable() { // this is inner class you only can use final variable inside this

        @Override
        public void run() {

        }
    }).start();

Ответ 7

Кажется, в вашем сознании есть некоторая путаница в отношении того, как окончательная и неустойчивая работа в Java.

Сначала получим некоторую терминологию. SyncClass - это класс, который вы хотите сделать потокобезопасным. Это означает, что ответственность за получение "правильного" (в соответствии с некоторыми правилами, вызванными моделью памяти языка программирования) приводит к наличию нескольких потоков. Экземпляры SyncClass будут разделяться между несколькими потоками и будут по-прежнему давать прогнозируемые результаты (это можно объяснить).

OuterClass является клиентом SyncClass. Таким образом, OuterClass может полагаться на гарантии, что SyncClass обеспечивает безопасность потоков без необходимости выполнять так называемую "блокировку на стороне клиента". Блокировка на стороне клиента означает, что безопасность потока может быть достигнута клиентами.

Теперь у вас есть общие данные в виде экземпляров SyncClass. Объединив оба своих общих метода, вы требуете, чтобы только один поток выполнял их в любой момент времени. Таким образом, любой клиент (имеющий доступ к экземпляру SyncClass) может ожидать предсказуемого поведения из своего экземпляра.

Но так как эффекты потока, вызывающего метод init, не могут быть замечены другими потоками без летучих, во втором случае требуется волатильность. Вы также можете использовать конструктор для инициализации конечных (пустых конечных) переменных, которые являются общими.

Где конечный и неустойчивый нужен?

Ну, окончательный требуется, когда клиенты хотят использовать экземпляр класса как переменную таким образом, что он не может быть переназначен другому экземпляру.

Летучие довольно тонкие. На данный момент вы можете сказать, что если есть несколько потоков, которые используют этот экземпляр таким образом, что только один поток записывает на него, а другие только читают, то в этом случае вы можете объявить переменную volatile.