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

Я изучал это, чтобы понять поведение полей final в новом JMM (начиная с 5). Эта концепция ясна: гарантированная видимость инициализированных полей final для всех потоков после того, как объект будет правильно сконструирован.

Но потом, в конце раздела, я прочитал вот это, что меня просто сбивает с толку:

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

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

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


person haps10    schedule 06.07.2011    source источник


Ответы (4)


Семантика конечных полей, как определено в разделе 17.5 документа JLS, гарантируем, что:

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

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

Однако нет никакой гарантии, что объект будет видимым для данного потока. Это другая проблема.

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

Рассмотрим следующий код:

final class A {
  private final int x;
  A(int x) { this.x = x; }
  public getX() { return x; }
}

class Main {
  static volatile A a1 = null;
  static A a2 = null;
  public static void main(String[] args) {
    new Thread(new Runnable() { void run() { try {
      while (a1 == null) Thread.sleep(50);
      System.out.println(a1.getX()); } catch (Throwable t) {}
    }}).start()
    new Thread(new Runnable() { void run() { try {
      while (a2 == null) Thread.sleep(50);
      System.out.println(a2.getX()); } catch (Throwable t) {}
    }}).start()
    a1 = new A(1); a2 = new A(1);
  }
}

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

В этом коде мы можем быть уверены, что поток 1 завершит выполнение (то есть он увидит это a1 != null. Однако может случиться так, что поток 2 остановится, так как он никогда не увидит запись в поле a2, поскольку это не летучий.

person Bruno Reis    schedule 06.07.2011

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

Я бы немного подозрительно отнесся к тексту, который обычно не превращается в никакие другие в пространстве предложения. Фактически, это правда, зависит от того, что именно мы подразумеваем под «синхронизацией использования».

Соответствующие части спецификации языка Java:

Два действия могут быть упорядочены по отношению произойдет до. Если одно действие происходит до другого, то первое будет видно и упорядочено перед вторым.

а также

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

«Случается-раньше» можно установить несколькими способами:

Если у нас есть два действия x и y, мы пишем hb (x, y), чтобы указать, что x происходит до y.

  • Если x и y являются действиями одного и того же потока и x стоит перед y в программном порядке, тогда hb (x, y).
  • От конца конструктора объекта до начала финализатора (§12.6) для этого объекта существует граница «произошло раньше».
  • Если действие x синхронизируется со следующим действием y, то у нас также есть hb (x, y).
  • Если hb (x, y) и hb (y, z), то hb (x, z).

куда

Действия синхронизации вызывают отношение synchronized-with для действий, определяемое следующим образом:

  • Действие разблокировки на мониторе m синхронизируется со всеми последующими действиями блокировки на m (где последующие действия определяются в соответствии с порядком синхронизации).
  • Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими чтениями v любым потоком (где последующее определяется в соответствии с порядком синхронизации).
  • Действие, запускающее поток, синхронизируется с первым действием в потоке, которое оно запускает.
  • Запись значения по умолчанию (ноль, ложь или ноль) в каждую переменную синхронизируется с первым действием в каждом потоке. Хотя может показаться немного странным записывать значение по умолчанию в переменную до выделения объекта, содержащего переменную, концептуально каждый объект создается в начале программы с его инициализированными значениями по умолчанию.
  • Последнее действие в потоке T1 синхронизируется с любым действием в другом потоке T2, которое обнаруживает, что T1 завершился. T2 может сделать это, вызвав T1.isAlive () или T1.join ().
  • Если поток T1 прерывает поток T2, прерывание T1 синхронизируется с любой точкой, в которой любой другой поток (включая T2) определяет, что T2 был прерван (сгенерировано InterruptedException или вызвано Thread.interrupted или Thread.isInterrupted).

Делая поля окончательными, вы гарантируете, что их назначение произойдет до завершения конструктора. Вам по-прежнему необходимо убедиться, что завершение конструктора происходит до доступа к объекту. Если этот доступ происходит в другом потоке, вам необходимо установить synchronizes-with, используя любой из 6 способов, показанных выше. Обычно используются:

  1. Запустите поток чтения после завершения инициализации. На практике, инициализация объекта в основном потоке перед запуском других потоков позволяет добиться этого.
  2. Объявите поле, которое другие потоки используют для доступа к объекту volatile. Например:

    class CacheHolder {
        private static volatile Cache cache;
    
        public static Cache instance() {
            if (cache == null) {
                // note that several threads may get here at the same time,
                // in which case several caches will be constructed.
                cache = new Cache();
            }
            return cache;
        }
    }
    
  3. Выполните как начальное присвоение, так и чтение поля в синхронизированном блоке.

    class CacheHolder {
        private static Cache cache;
    
        public synchronized static Cache instance() {
            if (cache == null) {
                cache = new Cache();
            }
            return cache;
        }
    }
    
person meriton    schedule 06.07.2011
comment
В первом предложении: Обычно правильно. не должно быть другого пути правильно. Все 6 упомянутых вами способов - это действия по синхронизации. - person assylias; 18.01.2013
comment
Это зависит от определения использования синхронизации. Многие общие определения синхронизации (включая определение из Википедии) определяют ее как координацию уже существующих процессов или даже как использование мониторов для взаимного исключения (как ключевое слово synchronized в Java или учебное руководство по Java, когда в нем говорится о реентерабельная синхронизация). В частности, определение синхронизации JLS - единственное определение синхронизации, которое я когда-либо видел, которое включает запуск потока. - person meriton; 19.01.2013

Создание всех полей final обеспечит их правильную публикацию в других потоках. Этот комментарий, вероятно, относится к следующему сценарию:

private myField;

public void createSomething()
{
    myField = new MyImmutableClass();
}

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

person Martin    schedule 06.07.2011
comment
Мне кажется правильным, но я не уверен, применимо ли ваше описание к ситуациям вне конструкторов, например, когда последняя ссылка используется анонимным классом внутри метода. Как вы думаете, в таком случае явная синхронизация не требуется? - person Victor Sorokin; 06.07.2011

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

person Victor Sorokin    schedule 06.07.2011