Быстрая и правильная реализация Singleton в Java

от
Java    паттерны

images.jpg

Доброго времени суток!

Копаясь в интернете, я наткнулся на интересные статьи о Синглтонах. О быстрых, о медленных. Изучив их, и заметив некоторые недостатки, я решил написать свою версию синглтона, избавившись от части минусов.

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

Сингтон представляет из себя паттерн, гарантирующий существование только одного экземпляра класса, и предоставляющий к нему доступ.

Применение:
— Системе требуется только один экземляр класса.
— Экземпляр класса должен легко доступен все компонентам системы.
— Требуется глобальная переменная или её аналог.
— Требуется наследуемость и полиморфизм для статического класса, но это не поддерживается.

На языке java уже реализовано несколько версий этого шаблона:

  1. public class Singleton {
  2.     public static final Singleton SINGLETON = new Singleton();
  3. }

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

  1. public class Singleton {
  2.     private static Singleton instance;
  3.     private Singleton(){}
  4.     public static Singleton getInstance() {
  5.         if (instance == null) {
  6.             instance = new Singleton();
  7.         }
  8.         return instance;
  9.     }
  10. }

Уже лучше. Но имеет серьёзные недостатки, такие как отсутствие потокобезопасности. Можно сделать метод synchronized, но это серьёзно ограничит скорость выполнения из-за конкуренции потоков за вызов метода, даже тогда, когда это уже не нужно.

Рассмотрев несколько вариантов исправления этого недостатка, я первым делом изучил то, что было представлено как лучший синглтон до момента написания статьи:

  1. public class Singleton {
  2.     private static volatile Singleton instance;
  3.     private Singleton(){}
  4.     public static Singleton getInstance() {
  5.         if (instance == null) {
  6.             synchronized (Singleton.class) {
  7.                 if (instance == null) {
  8.                     instance = new Singleton();
  9.                 }
  10.             }
  11.         }
  12.         return instance;
  13.     }
  14. }

И разочаровался.

Этот вариант выглядит хорошо, но использовать его не рекомендуется, volatile модификатор может привести к проблемам производительности на мультипроцессорных системах. Нужно как-то решить проблему доступа к переменной, volatile блокирует кэширование значения переменной, заставляя каждый раз обращаться потоки к данным, блокируя их, и замедляя чтение для других потоков. Я решил исправить это и написать более лучший синглтон, чем уже имеющийся.

Первой пришла в голову мысль: а почему бы во время выполнения не передать значение volatile переменной в не volatile, всё равно после создания Объекта Синглтона переменная содержащая ссылку на Объект Синглтон не меняется и кеширование не повредит?

В исправил всё вот так:

  1. public class Singleton2 {
  2.     private static volatile Singleton2 instance2;
  3.     private static Singleton2 getInstance2() {
  4.         if (instance2 == null) {
  5.             synchronized (Singleton2.class) {
  6.                 if (instance2 == null) {
  7.                     instance2 = new Singleton2();
  8.                 }
  9.             }
  10.         }
  11.         return instance2;
  12.     }
  13.     private static Singleton2 instance;
  14.     public static Singleton2 getInstance() {
  15.         if (instance == null) {
  16.             instance = getInstance2();
  17.         }
  18.         return instance;
  19.     }
  20. }

Соединив потокобезопасные и не потокобезопасные методы мы получаем более высокую производительность при множественном обращении с минимумом использования volatile переменной блокирующей кэширование ссылки сохраняя при этом потокобезопасность. Однако данная реализация всё же не является самой быстрой. В голову пришла вторая мысль, что возможна быстрейшая реализация, когда код самомодифицируется и убирает условия оставляя только:

  1. public class Singleton {
  2.     private Singleton instance;//переменная уже содержит нужный объект
  3.     public Singleton getInstance() {
  4.         return instance;
  5.     }
  6. }

— Код после исполнения модифицирует себя, убирает ненужные инструкции перехода по условиям и работает очень быстро, — радовался я.

Однако подобное чревато очень большими рисками при использовании, т.е. не очень потокобезопасно, а при первом запуске очень медленно. К тому же такой код очень не красив и перегружен. И не static. Можете посмотреть код:

  1. public class SingletonBulder {
  2.     private static Singleton instance;
  3.     private static SingletonBulder.SB sb;
  4.     public SingletonBulder(){
  5.         sb=new SB();
  6.     }
  7.     public Singleton getInstance() {
  8.         return sb.getInstance();
  9.     }
  10.     private class SB{
  11.         public Singleton getInstance() {
  12.             instance=Singleton.getInstance();
  13.             sb=new SB(){
  14.                 public Singleton getInstance(){
  15.                     return instance;
  16.                 }
  17.             };
  18.             return instance;
  19.         }
  20.     }
  21. }

Придётся создать отдельный объект SingletonBulder, присвоив в самом начале инициализации final static полю к которому и обращаться. Т.е. создавать полусинглтон к создателю синглтона. К тому же, я не уверен, так ли быстр SingletonBulder, чтобы его использовать, а потому задействовал тесты для выяснения быстродействия.

Исходный код теста



One:25.71614 msc Two:2.066058 msc SB:6.242348 msc
Как и ожидалось, код с SingltonBuild оказался не таким быстрым, как виделся. Результаты после разогрева (нескольких запусков) примерно такие:


One:3.029997 msc
Two:1.213949 msc
SB:3.698755 msc

One:4.798618 msc
Two:1.538546 msc
SB:4.832412 msc

One:6.193176 msc
Two:1.893041 msc
SB:6.056945 msc

One:6.036351 msc
Two:1.828518 msc
SB:5.849464 msc

One:4.384834 msc
Two:1.328419 msc
SB:4.398327 msc


Не будь на java некоторых ограничений модикации скорость самомодифицирующегося кода была бы выше. Зато комбинация опасного кода с подушкой безопасности как и ожидалось за счёт возможности кэширования оказалась самой быстрой на момент тестирования. Поэтому можно сделать однозначный вывод, на данный момент моя модификация кода Singleton на java является быстрейшей.

Ах да, попрошу не забывать про приватный конструктор. В коде выше он был опущен, полный код выглядит так:

  1. public class Singleton {
  2.     private static volatile Singleton instance2;
  3.     private Singleton(){}
  4.     private static Singleton getInstance2() {
  5.         if (instance2 == null) {
  6.             synchronized (Singleton.class) {
  7.                 if (instance2 == null) {
  8.                     instance2 = new Singleton();
  9.                 }
  10.             }
  11.         }
  12.         return instance2;
  13.     }
  14.  
  15.     private static Singleton instance;
  16.  
  17.     public static Singleton getInstance() {
  18.         if (instance == null) {
  19.             instance = getInstance2();
  20.         }
  21.         return instance;
  22.     }
  23. }

Есть ещё более быстрый код Синглтона, но он не поддерживает ленивую инициализацию. Думаю много кто-предложит более быструю реализацию на java с сохранением всех нужных вещей. Но пожалуй я на этом кончу.
  • +4
  • views 7768