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

Всегда склонялся к варианту double check, но после статьи про текущую модель памяти в java, мир для меня перестал быть прежним. Оказывается, volatile довольно затратная операция и немного лучше synchronized. Наилучшим же вариантом с ленивой инициализацией будет следующий код:

public final class Singleton {
      private Singleton(){};
      
      public static Singleton getInstance(){
          return SingletonCreator.INSTANCE;
      }
      
      private static class SingletonCreator {
          private static final Singleton INSTANCE = new Singleton();
      }      
}

Этот вариант является thread-safe и lazy.

Потокобезопасный потому что java гарантирует что final поля будут проинициализированы только один раз и никакой потом не увидит null значения.

Ленивым этот вариант является потому что java инициализирует static поля у класса только в момент обращения к данному классу. Класс SingletonCreator ни в каких местах не доступен, кроме метода getInstance.