Thread Safe Java Singleton
Published: 2023-12-11 12:04:07

**Задача**. Что не так с этим кодом?

```java
    public class Singleton {
        private static Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
       // private constructor and other methods ...
    }
```

**Решение**. Этот код пытается создать Singleton в Java. Т.е. Java объект, который будет существовать только в одном экземпляре в программе. Причем делает это ленивым образом (lazy initialization). Он не создает объект с самого начала, а создает его лишь тогда, когда кто-то впервые вызовет метод getInstance().

Более того, он пытается сделать этот код потокобезопасным. Чтобы он работал правильно в многопоточной среде.

Подход использованный в этом коде называется Double-Checked Locking. Т.к. в коде дважды присутствует проверка instance == null. Это делается для того, чтобы избежать потенциально не нужного получения лока (например когда делаем весь метод getInstance() synchonized).

Например, два потока одновременно вызывают метод getInstance(). Пусть оба потока проверили, что instance == null. И для обоих потоков он null. Тогда какой-то из потоков получит лок на классе Singleton.class. А второй будет ждать. Первый поток выполнит код в synchronized блоке и инициализирует объект instance и отпустит лок. После второй поток получит лок, но когда второй раз выполнит проверку instance == null, то она уже вернет не null и повторное создание объекта уже не требуется.

Теперь другая ситуация. Снова два потока вызвали метод getInstance() практически одновременно. Пусть первый поток был чуть быстрее и успел выполнить первую проверку instance == null и выполнил весь synchronized блок. А второй поток только начал проверять intance == null в первом if. Для него intance будет уже не null. Тогда он пропустит synchonized блок и выполнит return instance. При этом он не будет получать никаких локов или их ждать.

**Т.е. все как будто работает как мы и хотели**. **Но** из-за того, что в описанном втором случае, создание и **запись в переменную делается в synchronized блоке**, **а чтение не в synchonized** блоке во втором потоке, **то Java не может гарантировать, что объект, который прочитает второй поток будет полностью инициализированным и корректно опубликованным (**смотри Java Memory Model, happens before и Safe Publication**).** Для этого надо или и запись и чтение помещать в synchronized блок или переменную, которую мы читаем сделать **volatile**. Volatile может решить проблему с visibility (но не с атомарностью) между потоками из-за возможных неконсистентных состояний кешей ядер процессора и проблемы re-ordering. До Java 1.5 Double-Checked Locking в Java не работал вообще. Начиная с Java 1.5 делая переменную volatile можно заставить такой код работать.

Т.е. код можно починить добавив volatile в объявление переменной instance:

```java
    public class Singleton {
        private static volatile Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
       // private constructor and other methods ...
    }
```

Как еще можно создать Singleton в Java?

Если среда **не многопоточная**, то:

```java
    public class Singleton {
        private static Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
       // private constructor and other methods ...
    }
```

Если многопоточная, то:

**Полностью сделать метод getInstance() synchronized** (но, тогда всегда нужно будет получать лок, что **будет сказываться на производительности программы**):

```java
    public class Singleton {
        private static Singleton instance;
    
        public synchronized static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
       // private constructor and other methods ...
    }
```

**Инициализировать объект сразу** (не использовать lazy-initialization), но тогда **объект будет создаваться всегда, даже если его никто не запросил**:

```java
    public class Singleton {
        private static Singleton instance = new Singleton();
    
        public static Singleton getInstance() {
            return instance;
        }
       // private constructor and other methods ...
    }
```

Для достижения lazy-initialization можно использовать внутренний класс:

```java
    public class SingletonFactory {
        private static class SignletonHolder {
          public static Singleton instance = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SignletonHolder.instance;
        }
    }
```

Еще вариант через Enum:

```java
    public enum EnumSingleton {
        
        INSTANCE("Initial class info"); 
     
        private String info;
     
        private EnumSingleton(String info) {
            this.info = info;
        }
     
        public EnumSingleton getInstance() {
            return INSTANCE;
        }
        
        // getters and setters
    }
    
    Использование:
    
    EnumSingleton.INSTANCE.getInstance()
```