понедельник, 7 февраля 2011 г.

5 способов реализации thread-safe синглтона на Java


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

Хорошую статью по реализации синглтонов на Java можно найти например,  здесь.

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

Наиболее частой проблемой при реализации многопоточного синглтона является попытка использовать т.н.  double-checked locking (подробно см. JSR 133, например тут). 

В чем же, в двух словах, проблема с этой двойной проверкой? Чисто технически, проблема тут в переупорядочивании инструкций (out-of-order writes), которую вправе делать javac или runtime JVM (JIT-компилятор).

(Еще точнее это разъяснено в комментариях к 133 JSR - "The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object")

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

1) Можно просто сделать getInstance() synchronized методом, как и рекомендуют очень многие авторы, и пусть потом профайлер попробует доказать, что именно этот метод является узким местом. В большинстве случаев эта реализация является самой простой и надежной.

2) Можно сделать поле instance volatile-переменной. 
Это будет работать в Java 1.5 и более новых версиях, так как именно в Java 1.5 была сделана ревизия Java Memory Model,  в которой понятие volatile field было более, скажем так, детерминировано - запись volatile поля работает (с точки зрения работы с памятью потока) так же, как и при синхронизации работает monitor release, т.е. сбрасывает кеш потока (ядра, процессора) в shared memory, а чтение volatile поля - как monitor acquire, т.е. инвалидейтит  кеш и заставляет поток перечитать данные из shared memory.

3) Не использовать отложенную инициализацию вовсе, а писать так: 
private static final Singleton instance = new Singleton(),  и никаких проблем не будет (ну кроме той, когда создание синглтона - дорогая операция, а он может и не понадобится в дальнейшем. С другой стороны, в реальных приложениях, как часто вы определяете синглтоны, которые нигде и никогда потом не используются? :)).

4) Четвертый, менее известный прием, заключается в том, чтобы использовать так называемый OnDemand Holder, который нередко называется по имени одного из создателей Java Memory Model - Bill Pugh's singleton. Он основан на механизме инициализации статических полей в Java. Эта самая инициализация происходит в момент загрузки класс в JVM, т.е. в нашем случае это будет при первом вызове метода getInstance (т.к. класс LazySomethingHolder внутренний и более нигде не используется). JVM в таком случае гарантирует, что статическое поле будет корректно инициализирована, и только после этого будет видимо для всех потоков. Такой трюк работает для всех версий Java, в отличие от volatile переменной.

public class Singleton{
    private Singleton(){}

private static class LazySomethingHolder {
  public static Singleton singletonInstance = new Singleton();
}

public static Something getInstance() {
  return LazySomethingHolder.singletonInstance ;
}

5) И, наконец, 5-й способ описал Джошуа Блох во втором издании своей книги "Effective Java". (В первый раз я забыл упомянуть - спасибо что подсказали в комментариях!).  На практике он распространен сравнительно мало, и знают его не все.
Этот способ работает только в Java 1.5+ и основан на том, что (собственно, с Java 1.5)  в Java появились Enum-ы. Поскольку енумы в Java являются тоже объектами (экземплярами класса  java.lang.Enum), они могут содержать любые данные и методы, а элемент енума по сути и является автоматически, singleton instance-ом. Этот подход имеют дополнительные преимущества в том плане безопасности при использовании Serializable и Reflection API. Вот тут есть хорошая статья (на английском) про реализацию синглтонов через енумы.



public enum Elvis {
  INSTANCE; // один элемент енума служит экземпляром синглтона

  // а дальше идут любые методы и данные
  public void leaveTheBuilding() {
    ...
  }
}

P.S. И помните, что паттерн Singleton имеет свои минусы, ибо глобален, зараза. И в ряде случаев  вместо него лучше использовать что-то более гибкое и настраиваемое, например, фабрики.

7 комментариев:

  1. а как же реализация через ENUM?

    ОтветитьУдалить
  2. Привет! Дполню PS - или DI-контейнеры :)

    ОтветитьУдалить
  3. Привет!

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

    А вообще полезное замечание, спасибо)

    ОтветитьУдалить
  4. Кстати, есть еще интересная тема насчет енумов, была в комментах по ссылке что я привел. Утверждается, что реализация синглтона через них самая мощная и защищенная, ибо енум нельзя дублировать с помощью обычной рефлексии - выскакивает ошибка специальная что, мол, "нельзя создать вручную объект енума через constructirObject.newInstance()".

    На самом деле если немного внутренней рефлексии использовать, из секретного военного пакета sun.reflect.*, то можно дублировать енум. Может заслуживает отдельной статьи.

    ОтветитьУдалить
  5. Вопрос только в том, кому в здравом уме придет в голову заниматься созданием дополнительных экземпляров синглтона?

    ОтветитьУдалить
  6. А что что означает instance?
    https://preply.com/question/chto-oznachaet-instance

    ОтветитьУдалить