Синглтон Java и синхронизация

117

Уточните, пожалуйста, мои вопросы по поводу синглтона и многопоточности:

  • Как лучше всего реализовать синглтон на Java в многопоточной среде?
  • Что происходит, когда несколько потоков пытаются одновременно получить доступ к getInstance() методу?
  • Можем ли мы сделать синглтоны getInstance() synchronized?
  • Действительно ли необходима синхронизация при использовании классов Singleton?
RickDavis
источник

Ответы:

211

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

Драконовская синхронизация:

private static YourObject instance;

public static synchronized YourObject getInstance() {
    if (instance == null) {
        instance = new YourObject();
    }
    return instance;
}

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

Дважды проверьте синхронизацию :

private static final Object lock = new Object();
private static volatile YourObject instance;

public static YourObject getInstance() {
    YourObject r = instance;
    if (r == null) {
        synchronized (lock) {    // While we were waiting for the lock, another 
            r = instance;        // thread may have instantiated the object.
            if (r == null) {  
                r = new YourObject();
                instance = r;
            }
        }
    }
    return r;
}

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

Инициализация по запросу :

private static class InstanceHolder {
    private static final YourObject instance = new YourObject();
}

public static YourObject getInstance() {
    return InstanceHolder.instance;
}

Это решение использует гарантии модели памяти Java при инициализации класса для обеспечения безопасности потоков. Каждый класс можно загрузить только один раз, и он будет загружен только тогда, когда это необходимо. Это означает, что первый раз getInstanceвызывается, InstanceHolderбудет загружен и instanceбудет создан, и, поскольку это контролируется ClassLoaders, никакой дополнительной синхронизации не требуется.

Джеффри
источник
23
Предупреждение - будьте осторожны с перепроверенной синхронизацией. Он не работает должным образом с JVM до Java 5 из-за «проблем» с моделью памяти.
Stephen C
3
-1 Draconian synchronizationи Double check synchronizationgetInstance () - метод должен быть статическим!
Grim
2
@PeterRader Они не должны быть такими static, но это могло бы иметь больше смысла, если бы они были. Внесены по запросу.
Джеффри
4
Ваша реализация блокировки с двойной проверкой не гарантирует работу. Фактически это объясняется в цитированной вами статье о блокировке с двойной проверкой. :) Там есть пример использования volatile, который правильно работает для 1.5 и выше (блокировка с двойной проверкой просто нарушается ниже 1.5). Инициализация по требованию держателя, также цитируемая в статье, вероятно, будет более простым решением в вашем ответе.
stuckj
2
@MediumOne AFAIK, rне требуется для правильности. Это просто оптимизация, позволяющая избежать доступа к изменчивому полю, поскольку это намного дороже, чем доступ к локальной переменной.
Джеффри
69

Этот шаблон выполняет поточно-безопасную отложенную инициализацию экземпляра без явной синхронизации!

public class MySingleton {

     private static class Loader {
         static final MySingleton INSTANCE = new MySingleton();
     }

     private MySingleton () {}

     public static MySingleton getInstance() {
         return Loader.INSTANCE;
     }
}

Это работает, потому что он использует загрузчик классов, чтобы выполнить всю синхронизацию бесплатно: к классу MySingleton.Loaderсначала обращаются внутри getInstance()метода, поэтому Loaderкласс загружается, когда getInstance()вызывается в первый раз. Кроме того, загрузчик класса гарантирует, что вся статическая инициализация завершена до того, как вы получите доступ к классу - это то, что обеспечивает безопасность потоков.

Это похоже на волшебство.

На самом деле он очень похож на шаблон enum Jhurtado, но я считаю, что шаблон enum является злоупотреблением концепцией enum (хотя он действительно работает)

Богемный
источник
11
Синхронизация все еще присутствует, она просто выполняется JVM, а не программистом.
Джеффри
@Jeffrey Вы, конечно, правы - я все это набирал (см. Правки)
Bohemian
2
Я понимаю, что это не имеет значения для JVM, я просто говорю, что это имело значение для меня в том, что касается самодокументированного кода. Я просто никогда не видел в Java заглавных букв без ключевого слова final (или enum), у меня был небольшой когнитивный диссонанс. Для тех, кто занимается программированием на Java на постоянной основе, это, вероятно, не будет иметь значения, но если вы будете переключаться между языками взад и вперед, это поможет быть ясным. То же для новичков. Хотя, уверен, к этому стилю можно приспособиться довольно быстро; все заглавные буквы наверное достаточно. Не хочу придираться, мне понравился твой пост.
Руби
1
Отличный ответ, хотя я не понял его части. Не могли бы вы уточнить: «Кроме того, загрузчик классов гарантирует, что вся статическая инициализация завершена до того, как вы получите доступ к классу - это то, что дает вам потокобезопасность». , как это помогает в обеспечении безопасности потоков, я немного запутался в этом.
gaurav jain
2
На самом деле @ wz366, хотя в этом нет необходимости, я согласен по соображениям стиля (поскольку он фактически является окончательным, потому что никакой другой код не может получить к нему доступ) finalследует добавить. Готово.
Bohemian
21

Если вы работаете в многопоточной среде на Java и вам нужно гарантировать, что все эти потоки обращаются к одному экземпляру класса, вы можете использовать Enum. Это будет иметь дополнительное преимущество, помогая вам справиться с сериализацией.

public enum Singleton {
    SINGLE;
    public void myMethod(){  
    }
}

а затем просто пусть ваши потоки используют ваш экземпляр, например:

Singleton.SINGLE.myMethod();
jhurtado
источник
8

Да, нужно getInstance()синхронизировать. В противном случае может возникнуть ситуация, когда можно создать несколько экземпляров класса.

Рассмотрим случай, когда у вас есть два потока, которые вызывают getInstance()одновременно. Теперь представьте, что T1 выполняется сразу после instance == nullпроверки, а затем запускается T2. На данный момент экземпляр не создан или не установлен, поэтому T2 пройдет проверку и создаст экземпляр. Теперь представьте, что выполнение снова переключается на T1. Теперь синглтон создан, но T1 уже проверил! Он продолжит создание объекта снова! Создание getInstance()синхронизированных предотвращает эту проблему.

Есть несколько способов сделать синглтоны потокобезопасными, но сделать getInstance()синхронизацию, вероятно, проще всего.

Oleksi
источник
Поможет ли это, если поместить код создания объекта в блок Synchronized вместо того, чтобы выполнять синхронизацию всего метода?
RickDavis
@RaoG Нет. Вам нужна и проверка, и создание в блоке синхронизации. Вам нужно, чтобы эти две операции выполнялись вместе без перерывов, иначе может произойти описанная выше ситуация.
Oleksi
7

Одноэлементное перечисление

Самый простой способ реализовать синглтон, который является потокобезопасным, - использовать Enum

public enum SingletonEnum {
  INSTANCE;
  public void doSomething(){
    System.out.println("This is a singleton");
  }
}

Этот код работает с момента появления Enum в Java 1.5.

Двойная проверка блокировки

Если вы хотите создать «классический» синглтон, работающий в многопоточной среде (начиная с Java 1.5), вам следует использовать этот.

public class Singleton {

  private static volatile Singleton instance = null;

  private Singleton() {
  }

  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class){
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance ;
  }
}

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

Ранняя загрузка Singleton (работает даже до Java 1.5)

Эта реализация создает экземпляр singleton при загрузке класса и обеспечивает безопасность потоков.

public class Singleton {

  private static final Singleton instance = new Singleton();

  private Singleton() {
  }

  public static Singleton getInstance() {
    return instance;
  }

  public void doSomething(){
    System.out.println("This is a singleton");
  }

}
Дан Молдован
источник
2

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

public class MySingleton {

  private static final MySingleton instance;

  static {
     instance = new MySingleton();
  }

  private MySingleton() {
  }

  public static MySingleton getInstance() {
    return instance;
  }

}
Уша
источник
@Vimsha Еще пара вещей. 1. Сделать instanceфинал 2. Сделать getInstance()статику.
John Vint
Что бы вы сделали, если бы захотели создать поток в синглтоне.
Арун Джордж
@ arun-george используйте пул потоков, при необходимости - пул с одним потоком и окружите его значением while (true) -try-catch-throwable, если вы хотите, чтобы ваш поток никогда не умирает, независимо от того, какая ошибка?
tgkprog
0

Как лучше всего реализовать синглтон на Java в многопоточной среде?

Обратитесь к этому сообщению, чтобы узнать, как лучше всего реализовать Singleton.

Как эффективно реализовать одноэлементный шаблон в Java?

Что происходит, когда несколько потоков одновременно пытаются получить доступ к методу getInstance ()?

Это зависит от того, как вы реализовали метод. Если вы используете двойную блокировку без изменчивой переменной, вы можете получить частично сконструированный объект Singleton.

Обратитесь к этому вопросу для получения более подробной информации:

Почему в этом примере блокировки с двойной проверкой используется volatile

Можно ли синхронизировать getInstance () синглтона?

Действительно ли необходима синхронизация при использовании классов Singleton?

Не требуется, если вы реализуете синглтон следующими способами

  1. статическая инициализация
  2. перечисление
  3. Ленивая инициализация с инициализацией по запросу_holder_idiom

Обратитесь к этому вопросу для получения более подробной информации

Шаблон проектирования Java Singleton: вопросы

Равиндра бабу
источник
0
public class Elvis { 
   public static final Elvis INSTANCE = new Elvis();
   private Elvis () {...}
 }

Источник: Эффективная Java -> Пункт 2

Он предлагает использовать его, если вы уверены, что класс всегда останется singleton.

Ashish
источник