2012-05-22 6 views
7

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

Если у меня есть статический неустойчивыми в моем классе:

private static volatile MyObj obj = null; 

и в методе ниже я:

public MyObj getMyObj() { 
    if (obj == null) { 
     obj = new MyObj();// costly initialisation 
    } 
    return obj; 
} 

мне нужно синхронизировать, чтобы обеспечить только один поток записывает в поле , или будут ли какие-либо записи сразу видны другим нитям, оценивающим условие obj == null?

Другими словами: неустойчиво ли вам заставить синхронизировать доступ к записи по статической переменной?

ответ

8

Вам потребуется вид блокировки, обеспечивающий запись только одного потока в поле. Независимо от волатильности два потока могут «видеть», что obj имеет значение NULL, а затем оба начинают инициализацию с помощью вашего текущего кода.

Лично я бы один из трех вариантов:

  • Initialize на класс нагрузки (зная, что это будет лень, но не ленивым, как ждать, пока getMyObj сначала называется):

    private static final MyObj obj = new MyObj(); 
    
  • Использование безусловная блокировка:

    private static MyObj obj; 
    private static final Object objLock = new Object(); 
    
    public static MyObj getMyObj() { 
        synchronized(objLock) { 
         if (obj == null) { 
          obj = new MyObj(); 
         } 
         return obj; 
        } 
    } 
    
  • Используйте вложенный класс для лени таким образом:

    public static MyObj getMyObj() { 
        return MyObjHolder.obj; 
    } 
    
    private static class MyObjHolder { 
        static final MyObj obj = new MyObj(); 
    } 
    
+0

Я не могу пройти маршрут держателя: он заставляет singleton на моем внешнем классе, который требует гораздо большего мышления и изменений в общей структуре, которую я разрабатываю. Проблемой, которую я должен был решить, было создание атома статического экземпляра. Я использовал статический метод и объявил его ('obj') статическим окончанием, чтобы обеспечить атомное присвоение и одновременную безопасность. –

+0

@atc: Нет, маршрут держателя не заставляет внешний класс быть одиночным. Не совсем понятно, что вы имеете в виду здесь ... –

+0

Для ясности: эта переменная 'static volatile' изначально была на Factory-объекте, которая ввела его во время выполнения в экземпляры, которые он отвечал за создание. Это связано с детальностью реализации упомянутого staic var ('MyObj' выше). Использование конструктора 'private' для удовлетворения идиомы держателя означало, что я не мог сохранить преимущества наследования для Factory объекта. Вместо того, чтобы улаживать идиому держателя здесь, я думал, что поеду на синхронизацию с двойной проверкой, поскольку это имеет наименьшее трение с кодовой базой и было доказано, что оно работает. –

3

Да, вы должны абсолютно синхронизировать (или использовать лучшую идиому, такую ​​как Singleton Holder idiom). В противном случае вы рискуете несколькими потоками, которые инициализируют ваш объект несколько раз (а затем впоследствии используют разные экземпляры).

Рассмотрим последовательность событий, как это:

  1. Поток А входит getMyObj() и видит, что obj == null
  2. Thread B входит getMyObj() и видит, что obj == null
  3. Поток А конструирует new MyObj() - давайте назовем его objA
  4. Тема B конструкции a new MyObj() - назовем это objB
  5. Поток А назначает objA к obj
  6. резьбы Б назначает objB к obj (который не null больше в этой точке, так что ссылка на objA, присвоенный резьбы A, перезаписывается)
  7. Поток А выходит getMyObj() и начинается использовать objA
  8. Thread B выходит getMyObj() и начинает использовать objB

Этот сценарий может произойти с любым количеством потоков. Обратите внимание, что, хотя здесь для простоты я принял строгий порядок событий, в реальной многопоточной среде события 1-2, 3-4 и/или 7-8 могут частично или полностью перекрываться во времени, не меняя конца результат.

Пример к держателю идиомы:

public class Something { 
    private Something() { 
    } 

    private static class LazyHolder { 
     public static final Something INSTANCE = new Something(); 
    } 

    public static Something getInstance() { 
     return LazyHolder.INSTANCE; 
    } 
} 

Это гарантированно безопасна, так как INSTANCE является final. Модель памяти Java гарантирует, что поля final инициализируются и отображаются корректно для любого количества потоков при загрузке содержащего класса. Так как LazyHolder - private, на который ссылается только getInstance(), он будет загружен только тогда, когда вызывается getInstance(). И в этот момент, INSTANCE инициализируется в фоновом режиме и безопасно публикуется JVM.

+0

Я мог бы просто указать статическое поле и использовать статический инициализатор или статический метод для создания, позволяющий синхронизироваться и логику try/catch. Таким образом, это будет более читаемым + мне не нужно будет блокировать вызов 'getMyObj()', потому что статический инициализатор будет выполнен до того, как класс станет доступным. –

+0

@atc, ваше описание неоднозначно, поэтому я не могу оценить его надежность и жизнеспособность без кода. Обратите внимание, что идиома держателя также не использует блокировок (JVM будет блокировать построение «LazyHolder», один раз, но сам 'getInstance() не синхронизирован). –

0

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

Основная проблема в том, что этом блоке код:

if (obj == null) { 
    obj = new MyObj();// costly initialisation 
} 

не является атомарным. На самом деле это очень длинный путь от атомарного.

1

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

Представьте следующий поток выполнения (предполагается, что два потока T1 & Т2):

  1. OBJ является нулевым Initally.
  2. Т1: если (OBJ == NULL): ДА
  3. Т2: если (OBJ == NULL): ДА
  4. Т1: создает новый экземпляр MyObj и назначить его OBJ
  5. T2: также создает новый экземпляр MyObj и назначает его obj

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

0

Еще один способ справиться с этим двойной проверка (Примечание: работает только с Java 5 или более поздней, см here подробности):

public class DoubleCheckingSingletonExample 
{ 
    private static final Object lockObj = new Object(); 

    private static volatile DoubleCheckingSingletonExample instance; 

    private DoubleCheckingSingletonExample() 
    { 
     //Initialization 
    } 

    public static DoubleCheckingSingletonExample getInstance() 
    { 
     if(instance == null) 
     { 
      synchronized(lockObj) 
      { 
       if(instance == null) 
       { 
        instance = new DoubleCheckingSingletonExample(); 
       } 
      } 
     } 

     return instance; 
    } 
} 

Когда деЫпзЬапс вызываются из двух потоков одновременно, и будет первым см. экземпляр как null, другой входит в синхронизированный блок и создает экземпляр объекта. Другой будет видеть, что экземпляр больше не является нулевым и не будет пытаться его создать. Дальнейшие вызовы getInstance будут видеть, что экземпляр не является нулевым и не будет пытаться заблокировать вообще.

+0

Для двойной проверки блокировки, такой как JDK5, необходимо правильно работать: http://cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html –

+2

К сожалению, правильная ссылка находится здесь: http: //www.cs. umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html –

+0

@DavidHeffernan: спасибо за ссылку, не знали о «JDK5 или новее» -обязании. Я отредактирую ответ, чтобы включить эту информацию. – esaj

Смежные вопросы